ponewheel / android-ponewheel

pOneWheel Android app
MIT License
71 stars 25 forks source link

No stats after firmware update #86

Closed jj05y closed 4 years ago

jj05y commented 5 years ago

Hey, I just cloned the repo and while the app connects fine, there's no stats available. It may be the firmware update. Can you repro?

COM8 commented 5 years ago

To me it looks like the Onewheel now requires some kind of initialistaion to send values. Else it will just report 0 as value for each characteristic.

COM8 commented 5 years ago

grafik

beeradmoore commented 5 years ago

Just noticed the same thing with Onewheel Community Edition app. I only get firmware and hardware revision.

Almost all properties in LightBlue are also unnamed now.

Going to investigate today with a phantom board and will let you know if I find anything useful.

kwatkins commented 5 years ago

Hmm well that's a bit annoying. If not solved I'll tackle in a few days, my XR is out as I change to a new treaded tire (currently stuck at the dismount step, it isn't easy as YouTube peeps show). I doubt the GUIDS changed but hard to say ... Likely will just reverse the official app.

beeradmoore commented 5 years ago

GUIDs are the same. Only firmware and hardware revisions have values via LightBlue.

The latest official app doesn’t detect my phantom board for doing the reverse engineering of their BLE anymore. Previous app versions still work fine.

Another thing is the URL path for the new firmware has changed. And a few other things about the apps are different. I wonder if all of this is FM trying to prevent these 3rd party apps existing?

EDIT: I updated my Android device to the latest OW app. I enabled HCI logging and then connected to the board and then sent the log to my computer and am stepping through it with Wireshark. I can see the app fetching ride mode fine and on the correct characteristic uuid. I am looking up back further to see if the board is sending any specific data to allow it to send the other data. Will update when I know more.

jj05y commented 5 years ago

Using gatttool its reading 0 for alot of handles, some have data but how is one to map them to meaningful names?

beeradmoore commented 5 years ago

Inspecting a bluetooth log, I am 99% confident the uuids have not changed and don’t need to be renamed.

The board is doing something else.

Should also be noted the old OW app can’t connect to the new OW board, much like the same issue we are in.

COM8 commented 5 years ago

Yes I can confirm this. The characteristics are still the same. Never the less they added 4: e659f31d-ea98-11e3-ac10-0800200c9a66 labeled Data29 e659f31e-ea98-11e3-ac10-0800200c9a66 labeled Data30 e659f31f-ea98-11e3-ac10-0800200c9a66 labeled Data31 e659f320-ea98-11e3-ac10-0800200c9a66 labeled Data32 Guess they are for the custom riding mode.

COM8 commented 5 years ago

Inspecting a bluetooth log, I am 99% confident the uuids have not changed and don’t need to be renamed.

The board is doing something else.

Should also be noted the old OW app can’t connect to the new OW board, much like the same issue we are in.

Do you mind uploading your HCI log? Don't have an Adroid device on hand, only Android x86 😉 and it's not letting me log traffic.

beeradmoore commented 5 years ago

e659f31d through to e659f320 are not new. LightBlue used to have named properties and it would call them unknown 1 to unknown 4. I don't believe I've ever seen them change value, but they could very we be used now though.

Log is attached, it can be opened by Wireshark. It also shows my device picking up a bunch of other devices so need to double check for service UUID and then compare it. From memory board connects in frame 117. btsnoop_hci.log.zip

EDIT: There is also bluetooth logging for iOS. I didn't know it was a thing otherwise I probably would have tried this. https://developer.apple.com/bug-reporting/profiles-and-logs/

COM8 commented 5 years ago

Ok thanks, maybe I can spot something new.

COM8 commented 5 years ago

So that's what I've discovered: The app based on your logs provided sends the following data to the board:

  1. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0
  2. Write 0fc2 to e659f311-ea98-11e3-ac10-0800200c9a66 // Set firmware revision to 4034 (BASE10)
  3. Write 43:52:58:d8:82:11:d1:26:96:5f:9f:aa:72:fc:de:92:f3:25:3d:20 to e659f3ff-ea98-11e3-ac10-0800200c9a66 // Write Q1JYw5jCghHDkSbCll/Cn8KqcsO8w57CksOzJT0g (BASE64) to serial write
  4. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0
  5. Write 007 to e659f302-ea98-11e3-ac10-0800200c9a66 // Sets ride mode to 7
  6. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0
  7. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0
  8. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0
  9. Write 00 to e659f319-ea98-11e3-ac10-0800200c9a66 // Set live time odometer to 0

The interresting one is the serial data. I couldn't make sence of it, but it's probably just in a wrong endian right now.

COM8 commented 5 years ago

Could you please provide a few more log files with connection establishments so I can validate my theory?

beeradmoore commented 5 years ago

The serial write one is interesting. I've only seen the board do serial write for firmware deploying. I won't be able to get more logs until after work.

I'd be interested to know:

For other people to get the data its really easy on Android if you have dev mode unlocked. http://www.fte.com/WebHelp/BPA600/Content/Documentation/WhitePapers/BPA600/Encryption/GettingAndroidLinkKey/RetrievingHCIlog.htm

Ill go check on Facebook if I can get anyone else to generate a log.

EDIT: In case anyone comes across this thread, that above link shows how to enable HCI logging. Here is an app that can open that log file and allow you to share it. https://play.google.com/store/apps/details?id=com.github.akinaru.hcidebugger

beeradmoore commented 5 years ago

Here is another log. Here is what I have found so far. This could just be an order of operations in a multi threaded app, or it could be how it triggers the board to send some sort of data as a key. But on frame 342 it enabled read notifications on E659F3EE which is interesting because that IS a new characteristic which isn't exposed when we can the boards characteristics.

Once notifications are enabled it writes the boards firmware revision back to itself. After that 20 bytes of data is received as notifications from the previous subscription (below as each notification)

43:52 58:7f:8e 0c 4c:17 7a:22 a2:b2 32 e2:e2 e2:e2:f8:77:ca

After that 20 bytes of data is sent back to the board on E659F3FE (which as you said was serial read previously (its odd that the first 3 bytes match, they would be the characters CRX if that means anything to anyone? Some of the other bytes are in the string, others are not found. Also note it is ).

43:52:58:4a:8d:4c:93:ca:9c:75:bc:ba:73:87:53:e9:10:4b:49:28

Also note it is slightly different from the value in my last log. Its the same length and starts with the same 3 bytes.

43:52:58:d8:82:11:d1:26:96:5f:9f:aa:72:fc:de :92:f3:25:3d:20

And for reference, from scan 1, here is the 20 bytes that where sent from the board to the phone.

43:52 58:7f 9e:5c 14 df:42 e2:62 82:62 62 62:62:62:77:f6:9c

After that the E659F3EE characteristic which I believe is the new serial read starts spitting out the gibberish it used to. I reckognise it because it'd keep saying "One" and some data between it. I never did figure out what it all meant. At frame 382 the notification for E659F3EE is disabled and then it seems that the board communicates as usual with its reads and writes how our apps used to.

After work tomorrow I am going to double check these values along with the endianness and see what they could possibly be.

btsnoop_hci-2.log.zip

COM8 commented 5 years ago

I will try to reverse engineer the .apk. Maybe I can spot something there.

beeradmoore commented 5 years ago

I had tried that a few days ago, but It appears to be obfuscated very differently to how it used to be. Either that or my new dev setup isn't working right.

jj05y commented 5 years ago

Yeah, it's been minified :( Makes it very hard to read.

beeradmoore commented 5 years ago

All this makes me think they really don't want 3rd party apps. Surely this wasn't all on accident :P

COM8 commented 5 years ago

A little bit more "security" is not wrong. They had some really big flaws, because everybody was able to connect to your oneweel and for example change light mode or change riding mode during the ride. Still not great. Would have hoped for some kind of pairing mechanism instead...

COM8 commented 5 years ago

*had and still have

beeradmoore commented 5 years ago

You're right, extra security is never a bad thing.

If only there was a bug bounty program or similar to receive free XRs.

kwatkins commented 5 years ago

Awesome everyone here is tackling this... I just did an initial look at the APK (using jadx for decompilation) and looks like they are doing some encoding/decoding with the chars on the top layer. I take it the messages are still coming through, just garbage until we figure out the seed and key values.

They also moved a lot of the methods handling sensitive and private data to native, just making it slightly more annoying to reverse, including MainActivity.getChallengeResponsePassword below.

com.rideonewheel.onewheel.OnewheelService public void onCharacteristicChanged(BluetoothGatt bluetoothGatt, BluetoothGattCharacteristic bluetoothGattCharacteristic) { this.a.a(bluetoothGattCharacteristic, 0); if (bluetoothGattCharacteristic.getUuid().equals(g.y) != null) { bluetoothGatt = e.a(); if (!bluetoothGatt.G()) { bluetoothGattCharacteristic.getStringValue(0); bluetoothGatt.a(bluetoothGattCharacteristic.getValue()); if (bluetoothGatt.I().length == 20) { String str = new String(Arrays.copyOfRange(bluetoothGatt.I(), 0, 3)); bluetoothGatt = Arrays.copyOfRange(bluetoothGatt.I(), 3, 19); byte[] decode = BaseEncoding.base16().decode(MainActivity.getChallengeResponsePassword(p.a(this.a.getApplicationContext()))); decode = Bytes.concat(bluetoothGatt, decode); bluetoothGattCharacteristic = Bytes.concat(bluetoothGattCharacteristic, com.rideonewheel.onewheel.shared.a.a(decode)); decode = new byte[]{(byte) 0}; for (byte b : bluetoothGattCharacteristic) { decode[0] = (byte) (b ^ decode[0]); } this.a.a(Bytes.concat(bluetoothGattCharacteristic, decode)); this.a.D(); } } } }

COM8 commented 5 years ago

Going to continue tomorrow, here's what I've found out so far: Used dex-tools and jd-gui. If you take a look into OnewheelService.java Line 1745 (if they are the same :D)

 public void a(byte[] paramArrayOfByte)
  {
    if (this.D != null) {
      a(this.D, paramArrayOfByte);
    }
  }

this.D is the serial write characteristic instance and we write an array of bytes to it. Thats the only time I found, that D really gets used for something. The only time this method gets called (no external call found) is in line 280:

  private e.a af = new e.a()
  {
    public void a()
    {
      OnewheelService.b(OnewheelService.this);
    }

    public void a(byte[] paramAnonymousArrayOfByte)
    {
      OnewheelService.this.a(paramAnonymousArrayOfByte);
    }
  };
jj05y commented 5 years ago

The reading/writing of firmware version is interesting. I thought It may be so each device knows the protocol to communicate with (because it changed with gemini). I tried this:

[0C:AE:7D:ED:07:xx][LE]> char-read-hnd 0059 Characteristic value/descriptor: 10 26 [0C:AE:7D:ED:07:xx][LE]> char-write-req 0059 1026 Characteristic value was written successfully

but still get 0 values for 45, 49 (roll, yaw) (and others) [0C:AE:7D:ED:07:xx][LE]> char-read-hnd 0045 Characteristic value/descriptor: 00 00 [0C:AE:7D:ED:07:xx][LE]> char-read-hnd 0049 Characteristic value/descriptor: 00 00

I can see in wireshark the equivalant of com8's step three above, some crazy value gets written. I think this is the key to the handshake, image

from the decomiled src of gemini ap: public static final UUID c = UUID.fromString("E659F300-EA98-11E3-AC10-0800200C9A66"); public static final UUID x = UUID.fromString("E659F3FF-EA98-11E3-AC10-0800200C9A66");

c is a brand new UUID, not ref'd in ponewheel src, x is ref'd as OnewheelCharacteristicUartSerialWrite

I'm at a loss at this point,

beeradmoore commented 5 years ago

E659F300-EA98-11E3-AC10-0800200C9A66 is the service UUID. I have E659F3FF-EA98-11E3-AC10-0800200C9A66 in OWCE listed as SerialWrite which is a named value via LightBlue, which is named by someone in the community I think... Maybe its named from the board itself.

I just got sent someone elses log and I got the same thing, read 20 bytes, did something and then sent those 20 bytes off.

Could you find E659F3EE-EA98-11E3-AC10-0800200C9A66 anywhere?

EDIT: @jj05y yeah I think writing of the board firmware version back is triggering the board to then send us the password for the device. And yeah reading it is for figuring out what flow we want to then follow makes sense.

@kwatkins Can we able to see the contents of the getChallengeResponsePassword like your above code or does it not get decompiled?

kwatkins commented 5 years ago

fyi, just trying to work out the flow of what the official app is doing, may/may not help the research going on now. let me know if there are places to zero on and i can continue the dive.

11-19 14:54:47.931 29257 29329 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:47.934 29257 29708 [com.rideonewheel.onewheel][com.rideonewheel.onewheel.MainActivity.getFirmwareFolderUrl] EjPXmbUBIJfrTYgBCw4AKPZMuzA=->https://s3-us-west-1.amazonaws.com/1wheel/fw_10-24-18/
11-19 14:54:47.937 29257 29328 [com.rideonewheel.onewheel][android.os.Parcel.writeString] com.rideonewheel.onewheel
11-19 14:54:47.939 29257 29329 [com.rideonewheel.onewheel][android.os.Parcel.writeString] 50:33:8B:6C:XX:XX
11-19 14:54:47.942 29257 29708 [com.rideonewheel.onewheel][java.net.HttpURLConnection] https://s3-us-west-1.amazonaws.com/1wheel/fw_10-24-18/version_XR.txt
11-19 14:54:47.942 29257 29708 [com.rideonewheel.onewheel][java.net.HttpURLConnection] https://s3-us-west-1.amazonaws.com/1wheel/fw_10-24-18/version_XR.txt
11-19 14:54:47.952 29257 29328 [com.rideonewheel.onewheel][android.os.Parcel.writeString] com.google.android.wearable.app.cn
11-19 14:54:47.996 29257 29268 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f311-ea98-11e3-ac10-0800200c9a66,length=2,setValue==ECY=
11-19 14:54:47.996 29257 29268 [com.rideonewheel.onewheel][android.os.Parcel.writeString] 50:33:8B:6C:61:XX
11-19 14:54:47.998 29257 29268 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.writeCharacteristic] UUID=e659f311-ea98-11e3-ac10-0800200c9a66,getStringValue=&
11-19 14:54:48.131 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==CR
11-19 14:54:48.223 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==WH8=
11-19 14:54:48.223 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==nTk=
11-19 14:54:48.224 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==HwA=
11-19 14:54:48.225 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=1,setValue==q
11-19 14:54:48.226 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==efk=
11-19 14:54:48.227 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==GZk=
11-19 14:54:48.228 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==mZk=
11-19 14:54:48.229 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==mZk=
11-19 14:54:48.230 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=2,setValue==Dow=
11-19 14:54:48.231 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,length=1,setValue==~
11-19 14:54:48.239 29257 29269 [com.rideonewheel.onewheel][android.os.Parcel.writeString] com.rideonewheel.onewheel
11-19 14:54:48.242 29257 29269 [com.rideonewheel.onewheel][com.rideonewheel.onewheel.MainActivity.getChallengeResponsePassword] in:EjPXmbUBIJfrTYgBCw4AKPZMuzA=,out:D9255F0F23354E19BA739CCDC4A91765
11-19 14:54:48.248 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,setValue==Q1JYh0FDe+slyo8GHPTpqE7VzcU=
11-19 14:54:48.250 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.writeCharacteristic] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,getStringValue=CRX�AC{�%ʏ��N���
11-19 14:54:48.252 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,enabled=false
11-19 14:54:48.259 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f302-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.263 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f30c-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.266 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f303-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.270 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f30a-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.273 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f314-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.280 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f316-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.284 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f313-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.288 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f307-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.292 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f319-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.296 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f30b-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.300 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f312-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.303 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f30f-ea98-11e3-ac10-0800200c9a66,enabled=true
11-19 14:54:48.364 29257 29269 [com.rideonewheel.onewheel][android.bluetooth.BluetoothGatt.setCharacteristicNotification] UUID=e659f31e-ea98-11e3-ac10-0800200c9a66,enabled=true
kwatkins commented 5 years ago

@beeradmoore it's native (c++) code and will require a bit more work, IDA disassembling... can go that route if we need to, hoping we don't since it's a bit more involved... Hooked the in/outs of getChallengeResponsePassword above, your right about the 20 bytes, it's just now to figure out the where/what...

beeradmoore commented 5 years ago

@kwatkins is that via android device monitor?

So this is how I think it works, your log here, the two posted above and a 3rd I got from someone else all take the same flow. I could be completely wrong.

Fetch board firmware revision number to know if to use the last flow, or the new one below.

If its gemini enable notifications of characteristic value change setCharacteristicNotification UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66 enabled=true

Write the board firmware revision number back to itself. This seems like an odd thing to do but I think it triggers the board to send the key or whatever you want to call it. writeCharacteristic UUID=e659f311-ea98-11e3-ac10-0800200c9a66

20 bytes of data comes through. From what I've seen the first 3 bytes are always the same, its also interesting the code you posted above does some substring things and also skips those first 3 bytes. setValue UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66 (repeat)

Once we have that data I think it goes into getChallengeResponsePassword, which then outputs 20 bytes again. This is then sent back to the board writeCharacteristic UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66

And then disable notifications for that thing we enabled originally. setCharacteristicNotification UUID=e659f3fe-ea98-11e3-ac10-0800200c9a66,enabled=false

From then on just looking at random bits of data they all seem to be in the correct characteristics where they belong. So from what I can gather this is the unlock flow.

That is why I think the next step is figuring out what getChallengeResponsePassword is doing.

EDIT: Just seen your response. I wonder if there is any chances to dump the connect handshake many times and see if there is a correlation of what is happening. Hopefully its just bit shifting and not something way more complex.

OR I wonder if I can just send back 20 bytes of data and this board isn't actually decoding it. It's just obscurity for the sake of it. I doubt it but I'll try that next just to be sure.

muellergit commented 5 years ago

There is a report from a Gemini rider on Facebook OWOG that Ponewheel shows cell values with Gemini if the new FM app is connected first. Then he disconnected the FM app, turned Bluetooth off and back on, then connected Ponewheel and it showed cell voltages (and I assume the other parameters). But after a while Ponewheel went blank again.

So if this behavior is confirmed, perhaps FM added a bit of authentication handshake that needs to happen before parameters will be sent, and the authentication handshake might be a challenge/response that is done periodically from the Gemini firmware to the app in order for the firmware to continue sending the existing unchanged parameters to the app.

On Mon, Nov 19, 2018, 2:29 PM Fabian Sauter <notifications@github.com wrote:

Going to continue tomorrow, here's what I've found out so far: Used dex-tools and jd-gui. If you take a look into OnewheelService.java Line 1745 (if they are the same :D)

public void a(byte[] paramArrayOfByte) { if (this.D != null) { a(this.D, paramArrayOfByte); } }

this.D is the serial write characteristic instance and we write an array of bytes to it. Thats the only time I found, that D really gets used for something. The only time this method gets called (no external call found) is in line 280:

private e.a af = new e.a() { public void a() { OnewheelService.b(OnewheelService.this); }

public void a(byte[] paramAnonymousArrayOfByte)
{
  OnewheelService.this.a(paramAnonymousArrayOfByte);
}

};

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/ponewheel/android-ponewheel/issues/86#issuecomment-440031176, or mute the thread https://github.com/notifications/unsubscribe-auth/APHfOQsitaPmdTj7Bltp-hebYtmqS7eRks5uwxShgaJpZM4YnXma .

beeradmoore commented 5 years ago

HA! Can confirm. Just opened the OW app, connected, force closed, opened OWCE and it read all my data but then died about 15 seconds later. Maybe this handshake needs to happen regularly. I never bothered examining the bluetooth logs past the first connection point.

muellergit commented 5 years ago

Good news! It makes me think that FM may be trying to make the connection a bit more secure with a challenge/response "authentication" handshake instead of doing this to stop third-party apps entirely. And also confirms that cell data is still being sent when successfully "authenticated."

On Mon, Nov 19, 2018, 5:36 PM beeradmoore <notifications@github.com wrote:

HA! Can confirm. Just opened the OW app, connected, force closed, opened OWCE and it read all my data but then died about 15 seconds later. Maybe this handshake needs to happen regularly. I never bothered examining the bluetooth logs past the first connection point.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ponewheel/android-ponewheel/issues/86#issuecomment-440081738, or mute the thread https://github.com/notifications/unsubscribe-auth/APHfOWmNVZ3GBLKYw5ndAj4mgFh7-bfBks5uw0CbgaJpZM4YnXma .

kwatkins commented 5 years ago

Yeah likely an handshake, will watch it how often... and @beeradmoore the com.rideonewheel.onewheel.MainActivity.getChallengeResponsePassword passes in the same string for and looks like all the native methods (below) take in that same string, "EjPXmbUBIJfrTYgBCw4AKPZMuzA=" in my instance. If that's the same string from "Once we have that data I think it goes into getChallengeResponsePassword, which then outputs 20 bytes again." - it's cheating and ugly, but if we have to we can directly link in the native library with those methods and call them directly.

I'm actually using xposed, a hooking framework, targeting the hooks for the methods used.


public static native String getAppVersionReleaseUrl(String str);
    public static native String getChallengeResponsePassword(String str);
    public static native String getChangePublicVisibilityOfRideUrl(String str);
    public static native String getCreateUserCredentials(String str);
    public static native String getDiagnosticsEmail(String str);
    public static native String getDiagnosticsUrl(String str);
    public static native String getDummyEmail(String str);
    public static native String getFirmwareFolderUrl(String str);
    public static native String getFirmwareMetaDataFilenamePlus(String str);
    public static native String getFirmwareMetaDataFilenameXR(String str);
    public static native String getHashForOnlinePlatform(String str);
    public static native String getLeaderboardSpeedUrl(String str);
    public static native String getLeaderboardTotalOdometerUrl(String str);
    public static native String getLeaderboardUrl(String str);
    public static native String getLeaderboardUrlForUser(String str);
    public static native String getLoginUrl(String str);
    public static native String getLoginWebPageUrl(String str);
    public static native String getMessagingUrl(String str);
    public static native String getPicsUrl(String str);
    public static native String getRequestPasswordUrl(String str);
    public static native String getRideImportLocationPointsUrl(String str);
    public static native String getRideImportRidesUrl(String str);
    public static native String getStatsUrl(String str);
    public static native String getTrailUrlForId(String str);
    public static native String getTrailsCoordinatesUrl(String str);
    public static native String getTrailsUrl(String str);
    public static native String getUpdatePasswordUrl(String str);
    public static native String getUpdateUserProfileUrl(String str);
    public static native String getUploadRidesUrl(String str);
    public static native String getUserDataUrl(String str);
    public static native String getUserPicsTracksUrl(String str);
    public static native String getUsersUrl(String str);
beeradmoore commented 5 years ago

@muellergit but if you have a board that isn't connected to anything, I can connect to it from my official OW app just like I could if you had the older firmware. So the security doesn't stop anyone doing what they could do before. Just makes 3rd party apps not work. I see that as something they wanted to stop because people (me lol) had been throwing around reverse engineering the firmware to see what could be done about pushback.

Messing with firmware could lead to more accidents, board malfunctions, fire, etc. I don't think it's something I'd want to tinker with anymore.

kwatkins commented 5 years ago

This actually motivates more tinkering/uncovering the hood, which will lead to more digging around the app past just getting it to work for reading stats. They should of made an SDK available for 3rd parties, did they just expect us to go, eh, FM, you win, all the users and peeps using these apps - your out of luck...

So the string passed in to native functions (getChallengeResponsePassword) is called off the context, jadx did a bad job of disassembling, but It looks to be just building the hash from app package manager attributes (OW signature, package name, etc). I don't see specific device attributes, so we should be seeing the same 20 char seed/hash/whatever it is across install instances.

            // C1383p.m4107a(this.mContext)
            // C1562i.m4598a(dummyEmail, "MainActivity.getDummyEma…til.getKeyHash(mContext))");

    public static final java.lang.String m4107a(android.content.Context r2) {
        /* JADX: method processing error */
/*
Error: java.lang.NullPointerException
*/
        /*
        r0 = "context";
        kotlin.p059d.p061b.C1562i.m4602b(r2, r0);
        r0 = r2.getPackageManager();     Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = r2.getPackageName();    Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r1 = 64;     Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = r0.getPackageInfo(r2, r1);  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = r2.signatures;  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0 = r2.length;  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r1 = 0;  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        if (r0 <= 0) goto L_0x0037;  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
    L_0x0019:
        r2 = r2[r1];     Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0 = "SHA";  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0 = java.security.MessageDigest.getInstance(r0);    Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = r2.toByteArray();   Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0.update(r2);   Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = r0.digest();    Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0 = 2;  Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r2 = android.util.Base64.encodeToString(r2, r0);     Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        r0 = "Base64.encodeToString(md.digest(), Base64.NO_WRAP)";   Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        kotlin.p059d.p061b.C1562i.m4598a(r2, r0);    Catch:{ NameNotFoundException -> 0x0037, NameNotFoundException -> 0x0037 }
        return r2;
    L_0x0037:
        r2 = "";
        return r2;
        */
        throw new UnsupportedOperationException("Method not decompiled: com.rideonewheel.onewheel.support.p.a(android.content.Context):java.lang.String");
    }

EDIT: Further confirmed by hooks to the MessageDigest update(), note the base64 output above is the same that's passed into getChallengeResponsePassword(), so we have the logic to what's passed in. Now it's determining what is going on inside that function - back to @beeradmoore workflow question above. Also need to look into why it seems the value is different from what we get out of the getChallengeResponse and what is being passed to "e659f3ff-ea98-11e3-ac10-0800200c9a66" - could be some encoding f'up on my part? (9255F0F23354E19BA739CCDC4A91765 != to...)

[android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,setValue==Q1JYkHSUtFKlscY6l7EupxHYXg8=,getStringValue=CRX�t��R���:��.��^,hexValue=435258907494b452a5b1c63a97b12ea711d85e0f

11-19 16:49:14.496   389   437 D kwatts.xposed: [com.rideonewheel.onewheel][java.security.MessageDigest.digest] algorithm=SHA,digest_length=20,hexvalue=1233d799b5012097eb4d88010b0e0028f64cbb30,base64value=EjPXmbUBIJfrTYgBCw4AKPZMuzA=
11-19 16:49:14.496   389   437 D kwatts.xposed: [com.rideonewheel.onewheel][com.rideonewheel.onewheel.MainActivity.getChallengeResponsePassword] in:EjPXmbUBIJfrTYgBCw4AKPZMuzA=,  out:D9255F0F23354E19BA739CCDC4A91765
muellergit commented 5 years ago

Yes, FM has defended their firmware with cease and desist orders. That was me (name in Hindi) on FB that mentioned that to you when you brought up working on allowing your OWCE app to revert to older firmware versions.

I don't know much more about the cease & desist, but FM does apparently secretly monitor some FB groups and since this GitHub is open they could be listening here too. That's probably a bit paranoid, but it is possible so I'll say no more than that. It makes me not trust their intentions though.

On Mon, Nov 19, 2018, 6:02 PM beeradmoore <notifications@github.com wrote:

@muellergit https://github.com/muellergit but if you have a board that isn't connected to anything, I can connect to it from my official OW app just like I could if you had the older firmware. So the security doesn't stop anyone doing what they could do before. Just makes 3rd party apps not work. I see that as something they wanted to stop because people (me lol) had been throwing around reverse engineering the firmware to see what could be done about pushback.

Messing with firmware could lead to more accidents, board malfunctions, fire, etc. I don't think it's something I'd want to tinker with anymore.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/ponewheel/android-ponewheel/issues/86#issuecomment-440086782, or mute the thread https://github.com/notifications/unsubscribe-auth/APHfOV9RIlWL1TZDLgrjH2eIsF6BXrXPks5uw0aJgaJpZM4YnXma .

beeradmoore commented 5 years ago

Ah gotya, had always been curious if that was a real name or just symbols someone who wanted to remain nameless found on the internet :). Im happy to stop working on firmware related things, it's more of a wanting to know how things tick than it is to make the board hit 40mph. I'd be upset (and would judge the legality) if we got cease and desist for 3rd party apps.

I have no doubt they have people monitoring the FB groups, the GitHub repos, unsure how closely or how much they care though. I mean, the company came about because of people tinkering with things. If I were them I'd be happy knowing people love the product enough that they want to tinker with it and make these 3rd party apps to make the experience better.

kwatkins commented 5 years ago

if anything it would be legal behind any monitoring, doubt FM peeps are directly monitoring, or even want to know - it's one of those as soon as you know, your involved, and lawyers can pull you in... see: patents, as soon as you read or aware of it - even - and more so - if you are close to it, it's in your org, you are now in the lawyers depose pool.

beeradmoore commented 5 years ago

@kwatkins is your decompiled code above from the getChallengeResponsePassword method or is that before it?

I also tried to google getChallengeResponsePassword in case they did some copy/paste code but it doesn’t seem like it. That would have been handy 😂 maybe I need to google something similar to find it. Wouldn’t be the first developer to copy/paste from StackOverflow.

beeradmoore commented 5 years ago

Also, is anyone else here devs for other apps? I can see kwatkins contributes a lot to ponewheel. I’m here from Onewheel Community Edition.

Float deck or onewheelwear here? I’m going to email them tomorrow to let them know things is busted and to see if they had any thoughts as well.

COM8 commented 5 years ago

I'm the dev of UWP-Onewheel.

kwatkins commented 5 years ago

Good to meet you all... Ya I did pOnewheel originally then many other devs, better than I picked it up - and the wife made the UI tolerable, I suck at those.

I'm trying to carve time (and the 🦃) to get something rolling... Anyone here know python or maybe even gatttool to start prototyping the flow steps we are slogging through? I can try updating the pOnewheel code, just will take longer to test. We might have enough here to work if we pull in the native libs, but that won't transfer to other projects that need this as well.

jj05y commented 5 years ago

Hey! My first attempt at writing a onewheel app was when I looked to yours for a starting point just after the gemini update. Needless to say I didn't get far!!

I'm using gatttool now for attempted connections, learning as I go. It's real fast for testing the connection. If any one wants me to try anything let me know.

kwatkins commented 5 years ago

@bearadmoore that was Java decompiled, the in to getchallenge is a 20 byte hash of app metadata returning the same 20 bytes, this is good, means we are working with simple one way hashes.

jj05y commented 5 years ago

Info on the 20byte hash. below is a selection of packets captured from the hci log. I searched for all that contained handle: 0016.

Check out (as was thought) that it gets sent regularly, ~every 30seconds. 4 packets are evident here, can get more if needed.

handle16.txt

EDIT: The goods are at the bottom of each packet

COM8 commented 5 years ago

I've translated the code from @kwatkins into a little bit more java.

Context someContext;
kotlin.p059d.p061b.C1562i.m4602b(someContext, "context"); // Loads probably the context
PackageManager pm = someContext.getPackageManager();
string name = someContext.getPackageName(); // Name of the package
int flags = 64;  //Or 0b1000000
PackageInfo pInfo = pm.getPackageInfo(name, flags); // Flag 64 resonds to GET_RESOLVED_FILTER see: https://developer.android.com/reference/android/content/pm/PackageManager#GET_PERMISSIONS
Signature[] signatures = pInfo.signatures; // Is a problem, read the comments bellow the answer of https://stackoverflow.com/questions/5564953/what-does-packageinfo-signatures-return
if(signatures.length <= 0) {
    return ""; // Error case
}
else {
    MessageDigest md = MessageDigest.getInstance("SHA");
    byte[] sig = signatures[0].toByteArray();
    md.update(sig);
    string result = Base64.encodeToString(md.digest(), Base64.NO_WRAP);
    kotlin.p059d.p061b.C1562i.m4598a(s, "Base64.encodeToString(md.digest(), Base64.NO_WRAP)"); // Could not find anything about this :(
    return result;
}

Quiet strait forward. That's what we need to know:

beeradmoore commented 5 years ago

My little intro is I'm the dev behind Onewheel Community Edition (OWCE), not yet released but just on GitHub for testing. It bothered me that people would suggest 3rd party apps like ponewheel and then other users would be like "I can't find it in the AppStore!?". So I made a cross platform Xamarin app.

@kwatkins I was just going to mock up the handshake in an Android app (C#) to see if I could get it to unlock.

For pInfo.signatures, apps can be decompiled, they can still be recompiled? Modify something to just output the hashes of all those signatures. But the interesting thing here is either is this needs to be cross platform for the iOS app to generate the same hash OR the board expects two possible returns (or the board doesn't care, it just wants 20 bytes and this is a wild goose-chase).

Does java pass by reference? If so then it isn't doing anything to the hash because we already have it. Maybe its just storing it in secure storage. But then all of this seems to be static properties, why would it change on each board boot or from jj05ys log above, for every 30sec handshake.

Also for the Base64.encodeToString calls. The 2nd parameter of 2 is the same value for Base64.NO_WRAP.

kwatkins commented 5 years ago

Good questions, I think? I'm closer. I don't know. Still not getting the values coming in. I think it comes down to writeCharacteristic to the serial port, "e659f3ff-ea98-11e3-ac10-0800200c9a66" (OnewheelCharacteristicUartSerialWrite), stepped through below.

@COM8 those values start with what your looking it, a hash generated from the app metadata attributes... basically a verification that the app is legit, since it includes the app signatures (all apps are signed by devs, an unique public key). We can just copy the result as @beeradmoore mentioned, should be fine...

So we get the hash from the app metadata, this running under the OW official app, Android, latest version...

[java.security.MessageDigest.digest] algorithm=SHA,digest_length=20,hexvalue=1233d799b5012097eb4d88010b0e0028f64cbb30,base64value=EjPXmbUBIJfrTYgBCw4AKPZMuzA=,ArraystoString=[18, 51, -41, -103, -75, 1, 32, -105, -21, 77, -120, 1, 11, 14, 0, 40, -10, 76, -69, 48]

Pass that into getChallengeResponsePassword, does something with that data - not sure what - but we always get the same out of "D9255F0F23354E19BA739CCDC4A91765"

[com.rideonewheel.onewheel.MainActivity.getChallengeResponsePassword] in:EjPXmbUBIJfrTYgBCw4AKPZMuzA=,  out:D9255F0F23354E19BA739CCDC4A91765

Then another hash gets created, not sure with what data, likely uses the output above, but I noticed it does change!

[java.security.MessageDigest.digest] algorithm=MD5,digest_length=16,hexvalue=9b8bb13c24de2df69e9ce28692026f5c,base64value=m4uxPCTeLfaenOKGkgJvXA==,ArraystoString=[-101, -117, -79, 60, 36, -34, 45, -10, -98, -100, -30, -122, -110, 2, 111, 92]

Then the serial write I was mentioning before, which includes the digest hash above with the same first three bytes (67,82,88). I thought I could just write this value before setting the notifications up, but that still doesn't seem to work. Worse, this value that is set changes periodically and is called

[android.bluetooth.BluetoothGatt.writeCharacteristic] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,setValue=Q1JYm4uxPCTeLfaenOKGkgJvXDA=,getStringValue=CRX���<$�-�→o\0,hexValue=4352589B8BB13C24DE2DF69E9CE28692026F5C30,ArraystoString=[67, 82, 88, -101, -117, -79, 60, 36, -34, 45, -10, -98, -100, -30, -122, -110, 2, 111, 92, 48]

Another run of the above, after restarting the app, you can see the same values are there until the last hash/serial write ...

[java.security.MessageDigest.digest] algorithm=SHA,digest_length=20,hexvalue=1233d799b5012097eb4d88010b0e0028f64cbb30,base64value=EjPXmbUBIJfrTYgBCw4AKPZMuzA=,ArraystoString=[18, 51, -41, -103, -75, 1, 32, -105, -21, 77, -120, 1, 11, 14, 0, 40, -10, 76, -69, 48]
[com.rideonewheel.onewheel.MainActivity.getChallengeResponsePassword] in:EjPXmbUBIJfrTYgBCw4AKPZMuzA=,  out:D9255F0F23354E19BA739CCDC4A91765
[java.security.MessageDigest.digest] algorithm=MD5,digest_length=16,hexvalue=ef269d4a58f96dee2e27c82aef207d9a,base64value=7yadSlj5be4uJ8gq7yB9mg==,ArraystoString=[-17, 38, -99, 74, 88, -7, 109, -18, 46, 39, -56, 42, -17, 32, 125, -102]
[android.bluetooth.BluetoothGattCharacteristic.setValue] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,setValue=Q1JY7yadSlj5be4uJ8gq7yB9mrY=,getStringValue=CRX�&�JX�m�.'�*� }��,hexValue=435258EF269D4A58F96DEE2E27C82AEF207D9AB6,ArraystoString=[67, 82, 88, -17, 38, -99, 74, 88, -7, 109, -18, 46, 39, -56, 42, -17, 32, 125, -102, -74]
[android.bluetooth.BluetoothGattCharacteristic.getValue] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,getStringValue=CRX�&�JX�m�.'�*� }��,hexValue=435258ef269d4a58f96dee2e27c82aef207d9ab6,ArraystoString=[67, 82, 88, -17, 38, -99, 74, 88, -7, 109, -18, 46, 39, -56, 42, -17, 32, 125, -102, -74]
[android.bluetooth.BluetoothGattCharacteristic.getValue] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,length=20,getStringValue=CRX�&�JX�m�.'�*� }��,hexValue=435258ef269d4a58f96dee2e27c82aef207d9ab6,ArraystoString=[67, 82, 88, -17, 38, -99, 74, 88, -7, 109, -18, 46, 39, -56, 42, -17, 32, 125, -102, -74]
[android.bluetooth.BluetoothGatt.writeCharacteristic] UUID=e659f3ff-ea98-11e3-ac10-0800200c9a66,getStringValue=CRX�&�JX�m�.'�*� }��,hexValue=435258ef269d4a58f96dee2e27c82aef207d9ab6,ArraystoString=[67, 82, 88, -17, 38, -99, 74, 88, -7, 109, -18, 46, 39, -56, 42, -17, 32, 125, -102, -74]

The code in ponewheel, that, after connecting, that will write the values (util/BluetoothUtilImpl/onServicesDiscovered:

  lc = owGatService.getCharacteristic(UUID.fromString(OnewheelCharacteristicUartSerialWrite));
                lc.setValue(new byte[] { 67, 82, 88, -108, 87, 76, 37, 33, 66, -127, 24, 76, -106, 101, 37, 66, 126, -97, 79, 111 });
                mGatt.writeCharacteristic(lc);

Maybe if somehow we figure out the algorithm that it's getting, pass in the values above, the notifies will start coming through? I'm just not confident that is the case, almost like I'm missing something big.

beeradmoore commented 5 years ago

I've setup BLE logging on iOS to see what it does and I've confirmed a few things. When the board boots and it's sitting there it won't send through any data on the serial read (E659F3FE) characteristic. As soon as you write the firmware version to it you'll suddenly get 20 bytes (and no more) of data over 9 or so notification updates. Because of this I was able to write to it, get the buffer, write to it again, rinse and repeat. I was able to output about 14 keys a second. Although knowing what the key is and how its generated is fairly useless, it is interesting (and still useless) to see that some of the data it sends repeats within the keys.

2018-11-21 20:54:46.442 OWUnlocker.iOS[658:81079] 43:52:58:5f:be:a2:8a:55:b8:08:68:c8:58:58:58:58:58:6c:cb:3a
2018-11-21 20:54:46.501 OWUnlocker.iOS[658:81079] 43:52:58:1e:dd:bf:67:32:95:ed:0d:cd:cd:cd:cd:cd:cd:e1:00:f4
2018-11-21 20:54:46.561 OWUnlocker.iOS[658:81079] 43:52:58:ff:de:c2:4a:15:79:d1:d1:b1:d1:d1:d1:d1:d1:e5:e4:ed
2018-11-21 20:54:46.651 OWUnlocker.iOS[658:81079] 43:52:58:1f:7e:62:0a:d5:39:91:b1:11:81:81:81:81:81:95:b4:3d

We already knew the first 3 bytes is the same for every request. The next 9 bytes are something random The next 5 bytes repeat, followed by 3 more seemingly random bytes.

Also here is a list of known input (20 bytes the board sends) and output (20 bytes that is sent back to the board) values as a hex string. I'll update this post if I generate more for validation.

Input  43:52:58:7f:9e:5c:14:df:42:e2:62:82:62:62:62:62:62:77:f6:9c
Output 43:52:58:d8:82:11:d1:26:96:5f:9f:aa:72:fc:de:92:f3:25:3d:20

Input  43:52:58:7f:8e:0c:4c:17:7a:22:a2:b2:e2:e2:e2:e2:e2:f8:77:ca
Output 43:52:58:4a:8d:4c:93:ca:9c:75:bc:ba:73:87:53:e9:10:4b:49:28

Input  43:52:58:be:3c:45:5d:2d:90:38:f8:78:38:38:38:38:38:4e:0c:ac
Output 43:52:58:c8:4b:77:d2:1d:fa:5c:a1:ab:7e:ee:1f:c8:2f:fa:19:55

Input  43:52:58:ff:fe:cb:12:dd:3f:b7:b7:b7:57:57:57:57:57:6c:6b:94
Output 43:52:58:bd:26:ed:86:75:c3:be:b7:ab:7f:78:8c:0b:b6:3c:85:22

I tried sending the same 20 bytes we received back to the board as well as sending a known good response key and neither worked so we do know we need to do something with the key.

OWUnlocker.log

beeradmoore commented 5 years ago

The only MD5 code I could find is in com.rideonewheel.onewheel.shared.a

public static byte[] a(byte[] paramArrayOfByte)
  {
    try
    {
      byte[] arrayOfByte = new byte['���'];
      MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
      paramArrayOfByte = new DigestInputStream(new ByteArrayInputStream(paramArrayOfByte), localMessageDigest);
      while (paramArrayOfByte.read(arrayOfByte) != -1) {}
      paramArrayOfByte.close();
      paramArrayOfByte = localMessageDigest.digest();
      return paramArrayOfByte;
    }
    catch (Exception paramArrayOfByte)
    {
      for (;;) {}
    }
    return new byte[16];
  }

Which I believe can be written better as

  public static byte[] doMD5Hashing(byte[] inputArray)
  {
    try
    {
      byte[] arrayOfByte = new byte['���'];
      MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
      DigestInputStream paramArrayOfByte = new DigestInputStream(new ByteArrayInputStream(inputArray), localMessageDigest);
      while (paramArrayOfByte.read(arrayOfByte) != -1) {}
      paramArrayOfByte.close();
      byte[] md5Hash = localMessageDigest.digest();
      return md5Hash;
    }
    catch (Exception err)
    {
      for (;;) {}
    }
    return new byte[16];
  }

Value of arrayOfByte is confusing, unsure if it's a decompile issue or not. The while (paramArrayOfByte.read(arrayOfByte) != -1) {} also seems odd, doing some CPU work to generate some entropy or missing something from the decompile?

That a method is called here

someArray = Bytes.concat(new byte[][]
    {
        theFirst3BytesThatWeKnowAreTheSame, // See note 1 below
        doMD5Hashing(Bytes.concat(new byte[][]
            {
                Arrays.copyOfRange(paramBluetoothGatt.I(), 3, 19), // Also see note 1 below
                BaseEncoding.base16().decode("D9255F0F23354E19BA739CCDC4A91765") // see note 2 below
            }))
    }
);

Note 1: @kwatkins "Then the serial write I was mentioning before, which includes the digest hash above with the same first three bytes (67,82,88)." theFirst3BytesThatWeKnowAreTheSame is 67,82,88 (although for me they are 43:52:58)

Note 2: As far as I know the decode there is the static string which is from getChallengeResponsePassword

I see this followed by

            paramBluetoothGattCharacteristic = new byte[1];
            paramBluetoothGattCharacteristic[0] = 0;
            int j = paramBluetoothGatt.length;
            int i = 0;
            while (i < j)
            {
              paramBluetoothGattCharacteristic[0] = ((byte)(paramBluetoothGatt[i] ^ paramBluetoothGattCharacteristic[0]));
              i += 1;
            }
            paramBluetoothGatt = Bytes.concat(new byte[][] { paramBluetoothGatt, paramBluetoothGattCharacteristic });

which could have its parameters written better, but I think its just calculating the final byte as a validation bit.

If all of this is correct then I think we have all the info we need. Its time for bed though, Ill keep messing with it tomorrow.

Edit: although if that is all correct then how would iOS have the same result from the SHA of the signatures. Unless it was hard coded knowing it’s harder to pull the info from an IPA.

If it doesn’t matter and it’s there for entropy and the important bit is the verification bit then why didn’t sending a known functional result not work...