These are just some things I discovered looking at the code of the Zero NextGen Android App. This is based on an older version of the app, build 526
or app version 1.0.252
to be exact, so not the latest. And due to the way Android Apps are assembled not everything can be extracted. Warning this post will contain a lot of code blocks!
Lets start with the buildConfig
itself, that is a dynamically created class containing some constants that hold information of the build of your app. These all change with every build you create. Think of app version code and name, app store package and name, build flavor etc.
public final class BuildConfig {
public static final String APPLICATION_ID = "com.zeromotorcycles.nextgen";
public static final String BUILD_TYPE = "release";
public static final boolean DEBUG = false;
public static final String FIREBASE_SENDER_ID = "844439813489";
public static final String FLAVOR = "";
public static final boolean MOCK_MOTORCYCLE = false;
public static final String STARCOM_P = "dave1";
public static final String STARCOM_U = "dave_test";
public static final int VERSION_CODE = 526;
public static final String VERSION_NAME = "1.0.252";
public static final String google_app_id = "844439813489";
}
Nothing too surprising or interesting… except maybe for that Starcom login in there :s. And that is just the beginning of the stuff that can be extracted from such a simple (badly protected) app. Just to be clear; an Android app should not leak this kind of information. There are tons of options available from code obfuscation to specific build flavours to encryption and passing credentials within the encrypted body instead of via URL path parameters. All options that Zero rather not used. Not sure why.
Starcom API hidden in plain sight
There is a StarcomApiService
interface in code that shows all the existing endpoints for that service including how users are authenticated. This information was used to create my Zero NG App. So we can access the starcom information directly and display that information in our preferred way, including scrolling in time. Most of the information on this service can be found on the Bitbucket project website of this Zero NG App.
A quick overview of the endpoints here from code. Note how even usernames and passwords are just HTTP path parameters so visible for everyone logging traffic on that network! Just be cautious about that, I would advise everyone not to use this app on public wifi networks.
public interface StarcomApiService {
@POST("api.php")
Observable<StarcomResponse> createUser(@NotNull @Field("newuser") String str, @NotNull @Field("newpassword") String str2, @NotNull @Field("vin") String str3, @NotNull @Field("passcode") String str4, @NotNull @Field("format") String str5, @NotNull @Field("source") String str6, @NotNull @Field("commandname") String str7);
@POST("api.php")
Observable<StarcomResponse> deleteUser(@NotNull @Field("targetuser") String str, @NotNull @Field("sid") String str2, @NotNull @Field("source") String str3, @NotNull @Field("format") String str4, @NotNull @Field("commandname") String str5);
@POST("api.php")
Observable<StarcomResponse> deregister(@NotNull @Field("sid") String str, @NotNull @Field("vin") String str2, @NotNull @Field("source") String str3, @NotNull @Field("format") String str4, @NotNull @Field("commandname") String str5);
@POST("api.php")
Observable<StarcomResponse> deregisterForNotifications(@NotNull @Field("sid") String str, @NotNull @Field("registrationid") String str2, @NotNull @Field("reasons") String str3, @NotNull @Field("source") String str4, @NotNull @Field("format") String str5, @NotNull @Field("commandname") String str6);
@GET("api.php")
Observable<List<StarcomUnit>> getUnit(@NotNull @Query("user") String str, @NotNull @Query("pass") String str2, @NotNull @Query("format") String str3, @NotNull @Query("commandname") String str4);
@GET("api.php")
Observable<List<LastTransmit>> lastTransmit(@NotNull @Query("user") String str, @NotNull @Query("pass") String str2, @Query("unitnumber") int i, @NotNull @Query("format") String str3, @NotNull @Query("commandname") String str4);
@POST("api.php")
Observable<StarcomSession> login(@NotNull @Field("user") String str, @NotNull @Field("pass") String str2, @NotNull @Field("format") String str3, @NotNull @Field("commandname") String str4);
@POST("api.php")
Observable<StarcomResponse> logout(@NotNull @Field("sid") String str, @NotNull @Field("format") String str2, @NotNull @Field("commandname") String str3);
@POST("api.php")
Observable<StarcomResponse> registerForNotifications(@NotNull @Field("sid") String str, @NotNull @Field("registrationid") String str2, @NotNull @Field("reasons") String str3, @NotNull @Field("source") String str4, @NotNull @Field("format") String str5, @NotNull @Field("commandname") String str6);
@POST("api.php")
Observable<StarcomResponse> setUnitPassword(@NotNull @Field("user") String str, @NotNull @Field("pass") String str2, @Field("unitnumber") int i, @NotNull @Field("vin") String str3, @NotNull @Field("passcode") String str4, @NotNull @Field("format") String str5, @NotNull @Field("commandname") String str6);
}
These services respond in json
(or XML
based on the params) which is then translated to some model classes we can find also in code. The two important ones are StarcomUnit
for the info of your motorcycle and LastTransmit
for the datapoints fetched for a specific point in time. Retrofit2
is used as an external library for these network calls within the app. They do implement their apps in Kotlin BTW and seem to use compat libraries. So not all is bad news.
public final class StarcomUnit {
@Nullable private final String active;
@Nullable private final String address;
@Nullable private final String icon;
@Nullable private final String name;
@SerializedName("unitnumber")
@NotNull private final String unitNumber;
@Nullable private final String unitType;
@Nullable private final String vehicleColor;
@Nullable private final String vehicleModel;
// some more code here
}
public final class LastTransmit {
private final int charging;
private final int chargingtimeleft;
@SerializedName("datetime_actual")
@Nullable private final String dateTimeActual;
@SerializedName("datetime_utc")
@Nullable private final String dateTimeUtc;
@Nullable private final Double latitude;
@Nullable private final Double longitude;
@NotNull private final String name;
private final int pluggedin;
private final int soc;
private final int tippedOver;
@SerializedName("unitnumber")
@NotNull private final String unitNumber;
// more code here
}
Note that there is also a ZeroApiService
implemented in similar fashion. However upon inspection that turned out to provide only news information. The articles at the bottom of the view of the App… That said I haven’t seen any updates in the 2 years I have this app & bike. Also it might be completely broken at this point cause I can’t even find it in the current release of the App. Nothing very useful anyway.
public interface ZeroApiService {
@GET("featured.php")
Observable<ZeroNewsResponse> featured(@Nullable @Query("intl") String str);
}
The complete endpoints and some extra information on the Starcom hardware incorporated in these motorcycles was found in some code Constants.
public static final String STARCOM_API_BASE_URL = "https://mongol.brono.com/mongol/";
public static final String STARCOM_UNIT_NAME = "496716 Helios CANbus 3g";
public static final int STARCOM_UNIT_NUMBER = 496716;
public static final String ZERO_API_BASE_URL = "https://www.zeromotorcycles.com/app/";
The ZeroMotorcycleService
found in code is for the Bluetooth connection, more on that later in this article. If there is anything to be explored further I’d say that is it.
Sport Plus mode does exist
Something that Zero themselves will always deny when asked about is the hidden Sport Plus mode. On the dashboard of the bike you can select riding modes. The in factory modes available are Rain, Street, Eco and Sport. And you get one extra slot for a custom mode that you can change using the app. If you look in code all these modes can be found including the parameters used for these. Below is the ride mode definition, so these params is what a RideMode
defines.
public RideMode(@NotNull RideModeType rideModeType, @NotNull String str, @NotNull TractionControlType tractionControlType2, @NotNull AbsControl absControl2, int i, int i2, int i3, int i4, int i5, int i6, @NotNull DashboardTheme dashboardTheme, @NotNull DashboardTheme dashboardTheme2) {
Intrinsics.checkParameterIsNotNull(rideModeType, Param.TYPE);
Intrinsics.checkParameterIsNotNull(str, "name");
Intrinsics.checkParameterIsNotNull(tractionControlType2, "tractionControlType");
Intrinsics.checkParameterIsNotNull(absControl2, "absControl");
Intrinsics.checkParameterIsNotNull(dashboardTheme, "currentDashboardTheme");
Intrinsics.checkParameterIsNotNull(dashboardTheme2, "originalDashboardTheme");
this.type = rideModeType;
this.name = str;
this.tractionControlType = tractionControlType2;
this.absControl = absControl2;
this.maxSpeed = i;
this.maxPower = i2;
this.maxTorque = i3;
this.neutralRegeneration = i4;
this.brakeRegeneration = i5;
this.swap = i6;
this.currentDashboardTheme = dashboardTheme;
this.originalDashboardTheme = dashboardTheme2;
}
And below is a list of all the modes that are defined within the app. Matching the order of these params given on the modes with the definition of the previous code block explains what does what. Or at least what param is what, there is still some guess work to what units are used.
public static final class Eco extends ZeroRideMode {
private Eco() {
RideModeType rideModeType = RideModeType.ZERO;
String string = BaseApplication.Companion.getInstance().getString(C1485R.string.ride_mode_eco);
Intrinsics.checkExpressionValueIsNotNull(string, "BaseApplication.instance);
super(rideModeType, string, TractionControlType.STREET, AbsControl.ON, 70, 60, 60, 80, 100, 1, DashboardTheme.DARKGREEN, null);
}
}
public static final class Rain extends ZeroRideMode {
public static final Rain INSTANCE = new Rain();
private Rain() {
RideModeType rideModeType = RideModeType.ZERO;
String string = BaseApplication.Companion.getInstance().getString(C1485R.string.ride_mode_rain);
Intrinsics.checkExpressionValueIsNotNull(string, "BaseApplication.instance);
super(rideModeType, string, TractionControlType.RAIN, AbsControl.ON, 60, 60, 60, 70, 70, 1, DashboardTheme.DARKBLUE, null);
}
}
public static final class Sport extends ZeroRideMode {
public static final Sport INSTANCE = new Sport();
private Sport() {
RideModeType rideModeType = RideModeType.ZERO;
String string = BaseApplication.Companion.getInstance().getString(C1485R.string.ride_mode_sport);
Intrinsics.checkExpressionValueIsNotNull(string, "BaseApplication.instance);
super(rideModeType, string, TractionControlType.SPORT, AbsControl.ON, 100, 80, 80, 80, 80, 1, DashboardTheme.LIGHTORANGE, null);
}
}
public static final class Street extends ZeroRideMode {
public static final Street INSTANCE = new Street();
private Street() {
RideModeType rideModeType = RideModeType.ZERO;
String string = BaseApplication.Companion.getInstance().getString(C1485R.string.ride_mode_street);
Intrinsics.checkExpressionValueIsNotNull(string, "BaseApplication.instance);
super(rideModeType, string, TractionControlType.STREET, AbsControl.ON, 80, 75, 75, 75, 75, 1, DashboardTheme.LIGHTBLUE, null);
}
}
However you can also clearly see that the Sport+ mode does exist and has values like 120 where the Sport mode stops at 100. What these values exactly refer to or how this mode can be activated is unclear to me. But it does exist.
public static final class SportPlus extends ZeroRideMode {
public static final SportPlus INSTANCE = new SportPlus();
private SportPlus() {
RideModeType rideModeType = RideModeType.ZERO;
String string = BaseApplication.Companion.getInstance().getString(C1485R.string.ride_mode_sport_plus);
Intrinsics.checkExpressionValueIsNotNull(string, "BaseApplication.instance);
super(rideModeType, string, TractionControlType.SPORT, AbsControl.ON, 120, 100, 100, 100, 100, 1, DashboardTheme.LIGHTORANGE, null);
}
}
Some information about Bluetooth protocol
I haven’t explored this fully so I might update here later on when I have more information on this. For now this is just some code I found that shows how the communication over Bluetooth is implemented. For example these CRC checks:
package com.zeromotorcycles.nextgen.utility;
public class CRC {
// chopped some code here for readability
public static long calculateCRC(Parameters parameters, byte[] bArr) {
byte[] bArr2 = bArr;
long access$100 = 1 << (parameters.width - 1);
long j = (access$100 << 1) - 1;
long access$000 = parameters.init;
for (byte b : bArr2) {
long j2 = ((long) b) & 255;
if (parameters.reflectIn) {
j2 = reflect(j2, 8);
}
access$000 ^= j2 << (parameters.width - 8);
for (int i = 0; i < 8; i++) {
access$000 = (access$000 & access$100) != 0 ? (access$000 << 1) ^ parameters.polynomial : access$000 << 1;
}
}
if (parameters.reflectOut) {
access$000 = reflect(access$000, parameters.width);
}
return (parameters.finalXor ^ access$000) & j;
}
public long init() {
return this.initValue;
}
// more chopped
public long finalCRC(long j) {
if (this.crcParams.reflectOut != this.crcParams.reflectIn) {
j = reflect(j, this.crcParams.width);
}
return (j ^ this.crcParams.finalXor) & this.mask;
}
public long calculateCRC(byte[] bArr) {
return finalCRC(update(init(), bArr));
}
public CRC(Parameters parameters) {
this.crcParams = new Parameters(parameters);
this.initValue = parameters.reflectIn ? reflect(parameters.init, parameters.width) : parameters.init;
this.mask = (parameters.width >= 64 ? 0 : 1 << parameters.width) - 1;
this.crctable = new long[256];
byte[] bArr = new byte[1];
Parameters parameters2 = new Parameters(parameters);
parameters2.init = 0;
parameters2.reflectOut = parameters2.reflectIn;
parameters2.finalXor = 0;
for (int i = 0; i < 256; i++) {
bArr[0] = (byte) i;
this.crctable[i] = calculateCRC(parameters2, bArr);
}
}
public byte finalCRC8(long j) {
if (this.crcParams.width == 8) {
return (byte) ((int) finalCRC(j));
}
throw new RuntimeException("CRC width mismatch");
}
public short finalCRC16(long j) {
if (this.crcParams.width == 16) {
return (short) ((int) finalCRC(j));
}
throw new RuntimeException("CRC width mismatch");
}
public int finalCRC32(long j) {
if (this.crcParams.width == 32) {
return (int) finalCRC(j);
}
throw new RuntimeException("CRC width mismatch");
}
}
Sorry for the big code block but since I haven’t extracted the useful parts just yet I’ll just leave the majority of code in here. Even the packages are included just in case someone wants to look these up. The below extract should for example explain the format of the bluetooth information packages received.
package com.zeromotorcycles.nextgen.utility;
public final class ZeroBtTypes {
public static final long DATA_TIMEOUT_SECONDS = 3;
public static final ZeroBtTypes INSTANCE = new ZeroBtTypes();
public static final int MAX_BODY_SIZE = 148;
public static final long RESPONSE_TIMEOUT_SECONDS = 3;
public static final long ZERO_BT_PKT_HEAD = 4059231480L;
public static final long ZERO_BT_PKT_TAIL = 4176802545L;
@NotNull private static final byte[] emptyBody = new byte[0];
public enum Packet {
ZERO_BT_PKT_NULL(0),
ZERO_BT_PKT_VER(1),
ZERO_BT_PKT_RESEND(2),
ZERO_BT_PKT_BKINFO(3),
ZERO_BT_PKT_PWRPCK(4),
ZERO_BT_PKT_BTSTAT(5),
ZERO_BT_PKT_MBB_RD(6),
ZERO_BT_PKT_MBB_DIG(7),
ZERO_BT_PKT_BMS_DIG(8),
ZERO_BT_PKT_UPLOAD(9),
ZERO_BT_PKT_DASH_STATS(10),
ZERO_BT_PKT_ECUS(11),
ZERO_BT_PKT_ERROR_CODES(12),
ZERO_BT_PKT_RIDE_MODES_MANUFACTURING(13),
ZERO_BT_PKT_HASH(14),
ZERO_BT_PKT_CMD_CH(15),
ZERO_BT_PKT_CON_BYTES(16),
ZERO_BT_PKT_MEMORY_READ(17),
ZERO_BT_PKT_MEMORY_ADD(18),
ZERO_BT_PKT_BMS_INDEX(19),
ZERO_BT_PKT_ECU_INDEX(20),
ZERO_BT_PKT_BOOT_LOADER(21),
ZERO_BT_PKT_DASH_GAUGES(16),
ZERO_BT_PKT_SCHED_CHARGE(17),
ZERO_BT_PKT_RIDE_MODES_CUSTOM(18),
ZERO_BT_PKT_ACK(19),
ZERO_BT_PKT_TIMESTAMP(20),
ZERO_BT_PKT_CHARGE_TARGET(21),
ZERO_BT_PKT_RIDE_SHARE(22),
ZERO_BT_PKT_RANGE_ESTIMATION(23),
ZERO_BT_PKT_LOG_INIT(24),
ZERO_BT_PKT_LOG_BLOCK(25),
ZERO_BT_PKT_LOG_DONE(26),
ZERO_BT_PKT_STARCOM(27);
//...
}
//...
}
And then for all the specific datatype packets there are also Model classes available in code. Sharing all this will be too much but should be easy to find in the app. Look for class names like ZeroPacket
, RideSharePacket
, AckPacket
etc.
There is a hidden Admin section in the Drawer
Also something that probably should ‘ve never been in the release flavour of this app is all the Admin references. From views in code like the AdminActivity
and it’s activity_admin.xml
layout in the resources folder. I did look for ways on how to access this information.
In the literals there is a reference for a drawer item for Admin
access. I can tell from code that this is an option in the left menu of the app. And all the other options I can find but not this one for some reason is not used.
<string name="drawer_account">Account</string>
<string name="drawer_admin">Admin</string>
<string name="drawer_faq">FAQ</string>
<string name="drawer_legal">Legal</string>
<string name="drawer_login">Log In</string>
<string name="drawer_need_help">Need Help?</string>
<string name="drawer_owner_support">Owner Support</string>
<string name="drawer_privacy_policy">Privacy Policy</string>
<string name="drawer_rate_app">Rate App</string>
<string name="drawer_roadside_assist_setup">Roadside Assist Setup</string>
<string name="drawer_send_feedback">Send Feedback</string>
<string name="drawer_settings">Settings</string>
<string name="drawer_starcom">@string/starcom</string>
<string name="drawer_support">Support</string>
<string name="drawer_terms_and_conditions">Terms & Conditions</string>
<string name="drawer_utilities">Utilities</string>
I can even find in code where the View for this Admin section is loaded from the menu click event and also that shows that it’s done in a similar way for all these left drawer menu options.
public final void onAdminClick() {
DrawerViewModelDelegate drawerViewModelDelegate = this.delegate;
if (drawerViewModelDelegate == null) {
Intrinsics.throwUninitializedPropertyAccessException("delegate");
}
drawerViewModelDelegate.onDrawerAdminClicked();
}
But that is where I got stuck. I can clearly see that the view exists and that there should be a menu option linking to it, but I can’t find out why it’s not showing on the App I have installed. Most likely this is just some random code that is left in the release but no longer accessible since the code that does handle this entry point might be available only in debug builds or other specific builds not released to the public.
And there is more
This is just the beginning of what can be extracted. At a glance I can for example also see SQL statements for the ride information that is stored on the device. If anything looks promising to be inspected in more detail I think it’s the Bluetooth communication protocol. Cause with that we might be able to hack into that and unlock fun stuff.
If anyone is wondering what tools were used for this or has any other questions or contributions, don’t hesitate to leave a comment. The Zero NG App I created is only possible because the community brought my attention to this open API on the EMF.
1 Comment on “What I’ve learned from reverse engineering the Zero NextGen App so far”