ponewheel / android-ponewheel

pOneWheel Android app
MIT License
72 stars 25 forks source link

Pint Support #109

Open ghost opened 4 years ago

ghost commented 4 years ago

Hey all,

I just got my pint in the mail today and suprise suprise my nor any 3rd party app works with it as far as I know. It is the same behavior as when gemini first dropped, where no values can be read unless the board is first unlocked by the official app.

I suspect the hash seed/key (not sure on terminology) just changed and the process is still the same but I'm not sure, I intend to investigate when I can.

I know for gemini we had a crack team working in a thread on here and I was kinda hoping to reassemble the gang, I wasn't sure how else to contact everyone. I can test anything anyone thinks of if I'm the only one with a pint board so far.

TomasHubelbauer commented 4 years ago

I have the same problem. πŸ™‚

ghost commented 4 years ago

Ok so update, I created and hci log of the official app connecting to a pint, got it off my phone, and I'm digging through it on my PC, all of which was way harder than expected. I am totally out of my element, have no idea what I'm doing, and am just kinda trying stuff to see if anything works. If any of yall wanna look though it your more than welcome, here is the log. btsnoop_hci.log

ghost commented 4 years ago

Ok so, if I'm reading this correctly, this is what happens. First thing it tries is to read the ridemode, and that returns 0 because the board is locked. I'm assuming this is a test to see if the board needs to be unlocked. It then requests the hardware & firmware revisions, and both of those work. It then turns on notifications for ridemode & lightmode for some reason, and then without any prompt from the board, sends 098e56c8c595bc9423ce87aea3bc3a45738c4278 to [UUID: e659f3ffea9811e3ac100800200c9a66], which I've never seen before. After that it writes 0 to the speed characteristic, which is weird, and then starts requesting characteristics as normal and they return correct values.

I have 2 logs and they both have the same behavior, and send the same value every time. this could be a lot easier than gemini. I'm out of time to actually try it today but let me know if anyone has luck with this.

ghost commented 4 years ago

Welp, that was an order of magnitude easier than I thought it would be, I have it working lol. Heres the workflow I'm using now:

First, check the firmware revision. If less than 4034, we're all good. if >= 4034 && < 5000, do the normal thing for gemini. If >= 5000, write 098e56c8c595bc9423ce87aea3bc3a45738c4278 as a byte array to the characteristic with UUID e659f3ff-ea98-11e3-ac10-0800200c9a66. In the oncharacteristicwrite callback for that UUID, you can start requesting values and everything works.

I accidentally left in my code from gemini for sending the key challenge every 15 seconds, and that worked and keeps the connection open, so that part seems to be the same.

If someone could double check all this for me that would be great lol, I still can't believe I figured this out, it works, and that it was that easy

muellergit commented 4 years ago

I wonder how the shaping mode switching works with the pint?

On Wed, Dec 4, 2019, 3:52 PM Nanoux notifications@github.com wrote:

Welp, that was an order of magnitude easier than I thought it would be, I have it working lol. Heres the workflow I'm using now:

First, check the firmware revision. If less than 4034, we're all good. if

= 4034 && < 5000, do the normal thing for gemini. If >= 5000, write 098e56c8c595bc9423ce87aea3bc3a45738c4278 as a byte array to the characteristic with UUID e659f3ff-ea98-11e3-ac10-0800200c9a66. In the oncharacteristicwrite callback for that UUID, you can start requesting values and everything works.

I accidentally left in my code from gemini for sending the key challenge every 15 seconds, and that worked and keeps the connection open, so that part seems to be the same.

If someone could double check all this for me that would be great lol, I still can't believe I figured this out, it works, and that it was that easy

β€” 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/109?email_source=notifications&email_token=ADY56OMLKZYTP3MY6EVGN4LQXAYDRA5CNFSM4JURU5MKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEF6Y66Y#issuecomment-561876859, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADY56OJJGNRF36ZPQFJDAI3QXAYDRANCNFSM4JURU5MA .

ghost commented 4 years ago

Its the same, theres 4 custom shaping modes, you can set values 5,6,7,and 8, which correspond to Redwood, Pacific, Elevated, and Skyline. I tried setting it to custom shaping and it didnt work lol, worth a shot.

TomasHubelbauer commented 4 years ago

I've try to validate your findings in my Web Bluetooth based version this weekend! I'm super excited to try it, hope it works for me as well. Thanks for finding all this out!

muellergit commented 4 years ago

So you can use the existing Ponewheel to change the mode once it is connected with your new code?

On Wed, Dec 4, 2019, 4:05 PM Nanoux notifications@github.com wrote:

Its the same, theres 4 custom shaping modes, you can set values 5,6,7,and 8, which correspond to Redwood, Pacific, Elevated, and Skyline. I tried setting it to custom shaping and it didnt work lol, worth a shot.

β€” You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ponewheel/android-ponewheel/issues/109?email_source=notifications&email_token=ADY56OKJD5FK5TOE2WSCN7DQXAZSXA5CNFSM4JURU5MKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEF62CBI#issuecomment-561881349, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADY56OIPLS3QXT7T7TFCSITQXAZSXANCNFSM4JURU5MA .

ghost commented 4 years ago

Well maybe I don't know, it worked right away in my app, the labels were just wrong. Fixing that rn.

tekkies commented 4 years ago

That array you send may specific to your board. If I read correctly, the number my app sends is only identical for the first 3 bytes (6 hex digits).

ghost commented 4 years ago

Well damn, I guess life is never that easy. That would make sense that it's different per board, but the app never requests board specific information before the key is sent, so I assumed it was hard coded. Maybe they are using the board's Bluetooth broadcast id or name or something? I don't know, unless someone makes a super lucky guess we're probably gonna have to tear the official app apart and see what it's doing.

tekkies commented 4 years ago

@Nanoux did you push your changes to a branch somewhere? Even hard coding for my board would be super useful. Do you want a copy of my trace for comparison?

ghost commented 4 years ago

Oh I just added like 5 lines to my app, I haven't put anything up yet. AFAIK ponewheel uses my connection code, so you might be able to just drop it in. I'll post it when I get home if ya really need it.

Also ya more traces would be great, especially for boards that aren't mine.

muellergit commented 4 years ago

The board's last 6 digits of the serial number is sent as the Bluetooth broadcast name OWxxxxxx.

On Thu, Dec 5, 2019, 9:36 AM Nanoux notifications@github.com wrote:

Oh I just added like 5 lines to my app, I haven't put anything up yet. AFAIK ponewheel uses my connection code, so you might be able to just drop it in. I'll post it when I get home if ya really need it.

β€” You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/ponewheel/android-ponewheel/issues/109?email_source=notifications&email_token=ADY56OL4ABGEB6LVHHMG5XTQXEUYRA5CNFSM4JURU5MKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEGBKARY#issuecomment-562208839, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADY56OJYYO7IWBXCRE53T4DQXEUYRANCNFSM4JURU5MA .

ghost commented 4 years ago

Ok update, I noticed that the official app connected way faster after the first connection, so I deleted my onewheel form the app and made a system trace of a first time pint connection, and sure enough its different!

For a first time connection, it tries to read the ridemode, gets 0, then gets the firmware & hardware revisions as before. However, after that its goes into the Gemini workflow, and sets notifications on for SerialRead, then writes the firmware version back on itself, and sure enough a flood of bytes comes flooding in gemini style. Once it gets 20 bytes it writes that same crazy string to the board, and then turns off SerialRead notifications, and then requests values as normal. I only have one log of this because its an absolute pain in the ass to get them on android and I'm still not clear on when they are actually generated, but I suspect the byte dump sent by SerialRead is the same every time since the app caches the key for subsequent connections. Since the process is identical to Gemini but apps still broke I assume they probably just changed the hash seed/key thing as I originally suspected. I'm going to have to dig through the gemini thread and see how they found that key the first time and see if I can do the same...

As for code to hard-code connect to one pint, heres all the bits I added:

@Override public void onServicesDiscovered(BluetoothGatt gatt, int status){ owGatService = gatt.getService(UUID.fromString(OWDevice.OnewheelServiceUUID));

        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {//make delay to avoid weird disconnection
            @Override
            public void run() {
                mGatt.readCharacteristic(owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicFirmwareRevision)));
            }
        },500);

    }

@Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic c, int status) { String characteristic_uuid = c.getUuid().toString();

        if (characteristic_uuid.equals(OWDevice.OnewheelCharacteristicFirmwareRevision)) {

            int version = Util.unsignedShort(c.getValue());
            if(version >= 5000){
                connectionMode = 2;

                byte[] plzwrk = Util.StringToByteArrayFastest("098e56c8c595bc9423ce87aea3bc3a45738c4278"); //insert your board's key here
                BluetoothGattCharacteristic lc = owGatService.getCharacteristic(UUID.fromString(OWDevice.OnewheelCharacteristicUartSerialWrite));
                lc.setValue(plzwrk);
                boolean worked = gatt.writeCharacteristic(lc);

            }

        }
    }

@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //Timber.i( "onCharacteristicWrite: " + status); //mOWDevice.processUUID(characteristic); if (characteristic.getUuid().toString().equals(OWDevice.OnewheelCharacteristicUartSerialWrite)){ //WERE CONNECTED MOTHER FUCKERS! whenActuallyConnected(); } }

At least I think thats it lol. I removed all code that wasn't relevant to pint but other stuff happens in those methods obviously. do everything you actually want with the board in wehnActuallyConnected().

As for toggling smartstop or whatever its called, I looked into that as well and.... its a hot mess omg. They are using the custom shaping attribute from gemini, and to turn it off you write 0300 and to turn it on you write 0301. However after each write, the app turns on notifications for that attribute, and first it sends back 0200 for some reason, and then either 0300 or 0301, whichever was just written. I assume this is verification the write worked but its jank af and I got some weird behavior in the official app toggling it on and off as fast as I could.

Heres my log for a first time connection, afaik the connection starts happening at line 1157 btsnoop_hci.log

ghost commented 4 years ago

Side note for anyone smarter than me reading this, it looks to me like the pint LED strips have full RGB, in theory it might be possible to set them to any arbitrary color if the firmware supports that on the board's end, but I have no idea how to figure that out....

ghost commented 4 years ago

So theres another thread over here for another app here, it doesnt look like they got as far though https://github.com/COM8/UWP-Onewheel/issues/6

ghost commented 4 years ago

Well I tried just straight up using the gemini code but with the first 3 bytes switched out for the ones in my board's pint code, still didn't get the correct response, so the hash key probably did actually change.

I'm gonna ping @kwatkins , @beeradmoore, and @COM8, since you all may be interested in how far I've gotten and I think I need your help to finish this off.

beeradmoore commented 4 years ago

Ohooo, nice work!

I had similar issues when trying to get OWCE to connect to the board, but I was also half way through re-writing all the BT code so that probably didn't help πŸ˜‚

I'm working on getting that BT code finished this weekend to get a build out to users, but then I'll revisit this thread and work on the Pint update.

I like the RGB LED thing, but I wouldn't want to go anywhere near having to load custom firmware (or even load current stable firmware) as to not get FM to dislike us more than they currently do.

Do you have somewhere I can DM/PM/Email you? I have an idea.

ghost commented 4 years ago

Ya my email is shenanouxgans@gmail.com and my discord is Nanoux #6524

tekkies commented 4 years ago

This additional hack (in blue) got my Pint to connect if I have "Default to OW+" enabled. The key came from my bluetooth hci log running regular app (it's my personal key - see above).

image

Update: It continued to stream data to the app for about 15 mins before I moved out of range. Update: Unobscure a bit more of the key

beeradmoore commented 4 years ago

That key is generated in the lines above it after retrieving information from the serial read characteristic. The key may work for yours but we need to figure it out on everyone elses device. Maybe the start of the key changed from 43:52:58 to 09:83:5.

I'm around thinking with some stuff today, I'll see what my log says when I connect to my Pint.

ghost commented 4 years ago

The start of the key definitely changed but that's not the only thing unfortunately, I tried that :/

tekkies commented 4 years ago

I saw a comment that the said the latest firmware requires an API call to get the key for you own OW. If that is the case, can we make that API call or would that be considered hacking?

biell commented 4 years ago

There is nothing to say that FM won't say it is unauthorized use of their API, but it would not likely stand up in court (I am not a lawyer). That puts the plaintiff in a powerful position however, b/c who wants to be the defendant. I don't know why FM is going to these lengths, but it does seem likely that they are trying to stop 3rd party apps. However, it is possible that is not their goal, and the reason for this change is that this is the first step in their efforts to help locate and/or brick stolen boards. Without knowing their motivation, we can only guess as to what their reaction would be to having this app connect to their API.

Regardless, someone should trace their network traffic to see what the API call is. Once we have the call which was made and the device information the connection was made for, then we can figure out what the call looks like. We need to know how we would make the API call, even if we don't plan on adding it to the code.

If FM is sending the serial number exactly as it is on the board to a RESTful API open to the world on their server, then they don't really have a case to say another app cannot do that.

If they are encrypting or salting a hash with a private key to obfuscate the serial number before sending it to their API, then that could change things. Although, I still think that is fine.

It is also possible that the keys are generated based on the serial number and we could just reverse engineer that, then code it into the app. To guess at any of that, we would need to send a group of serial numbers into the API and see what comes back.

I wish there was a security setting where you could copy a preference from another app, then we could just ensure people connect with the official app first, and pull it in. But alas, I doubt that will ever be allowed on android.

tekkies commented 4 years ago

I updated the image above to show a bit more of my key. image

tekkies commented 4 years ago

@biell Thanks for the write-up. I sent you a PM.

I did also wonder about parsing the btsnoop_hci.log file, but my Moto G (6) phone does not place the file in accessible storage - I have to generate the log then "Take a bug report" and mail it to myself :(.

beeradmoore commented 4 years ago

@tekkies , FM already consider the current handshake bypass that came with gemini hacking (or at least they were throwing around cease and desist and getting apps removed for it).

Just checked, and Pint is doing some API call. Start of the key matches the key you are sending so it is related.

ghost commented 4 years ago

Well y'all are right, I tried a first time connect to my pint in the official app with wifi & data off and it gave me an error message, that is absolutely wild. It may be possible to do whatever that api call is doing locally in the app though like you said. I suspect it is very similar to what is happening with gemini and they don't want people to be able to reverse engineer it from the app. Its pretty hard for me to imagine they aren't trying to lock us out at this point to be honest.

tekkies commented 4 years ago

(Being forced to their API would help them get a handle on warranty start dates.)

ghost commented 4 years ago

No that's why they make you register your board in order for the warranty to be valid.

I would be curious to know if the unlock key is stored in memory on each board or if the algorithm to figure it out is coded into the firmware and we could get it that way.

Also, if that doesn't work, I guess there's nothing stopping us from just sending a bunch of strings with random numbers and seeing what comes back, I doubt they check if the serial numbers are valid or not. Just don't accidently ddos them and get yourself in trouble.

beeradmoore commented 4 years ago

Unlock code for Pint will also work for the new XR (fixing #111). Code is generated on the board (hash used for gemini is used here too) and then sent to FM in exchange for a token which then unlocks the board.

EDIT:

@Nanoux I already tried that. Returns a 4XX or 5XX error code if serial is invalid or if apiKey is invalid. They are generating apiKey on their end and validating it with the one passed in to make sure they are correct. The return string however, no idea if that's static for everyone or if its a hash of the apiKey/serial.

Based on its length I assume its either SHA1 or just the same 40 random characters that everyone gets.

ghost commented 4 years ago

Oh nice work, I guess we're running out of options already. My question was clearly the board knows what the right token is and it's the same Everytime, is that token stored in it's memory or does it do the same think the API does locally?

beeradmoore commented 4 years ago

It stores it on disk. I wouldn't assume it expires. So only fetched once and then re-used.

tekkies commented 4 years ago

Well, if you want to give pint a try and you know your unlock code (e.g. from your hci snoop), I've got a branch at here that connects to the pint if I enter my board MAC and unlock-key in the new user pref "Firmware 4142+ unlock keys". YMMV.

I've done a bit of refactoring so the old unlock may inadvertently have broken (hope not).

Currently only cell 14 voltage shows - Pint has new Cell IDs between 14 and 280). I wonder if that is a Pint thing or a firmware thing.

beeradmoore commented 4 years ago

EDIT: USE AT YOUR OWN RISK. Future Motion are handing out IP bans for using the following code. The code in OWCE appears to still be 100% functional

Unlock system for Pint (and should be new XR) has been committed to OWCE. https://github.com/OnewheelCommunityEdition/OWCE_App/pull/30

Magic is in 2 parts. Part 1: https://github.com/OnewheelCommunityEdition/OWCE_App/blob/4fc1923323543db849deccbf2998646cdf1bae31/OWCE/OWCE/OWBoard.cs#L899-L914 If firmware is > 4141 (so this is the new 4241 firmware and the Pint 5XXX firmware) then we don't want to do the normal gemini handshake. Copy 16 bytes from position 3 of the byte array we got for gemini previously and make it a base16 string. This string is then exchanged with FM to fetch the unlock key.

Part 2: https://github.com/OnewheelCommunityEdition/OWCE_App/blob/4fc1923323543db849deccbf2998646cdf1bae31/OWCE/OWCE/OWBoard.cs#L981 Web request is sent to FM which contains the board name minus the OW at the start. I originally assumed this was a serial number but that is incorrect. I mimicked headers sent by OW (iOS) app to make this request be as stealthy as possible. The url can be found here. The response body is a json string with 1 item called key. This key is then sent back to the board how we normally would for gemini.

This key is then stored locally so we don't have to make this web request every time. I am unsure how often or if at all the code originally fetched from the board changes but I added this to removed the cached token if the key changes.

This has not been tested on other boards yet, nor has it been tested on 4210 HW XRs.

COM8 commented 4 years ago

@beeradmoore Awesome! Currently porting it over to my app. Can you/somebody else give an example JSON response from the API? When I try it with: https://app.onewheel.com/wp-json/fm/v2/activation/12345?owType=xr&apiKey=00000000000000000000000000000000 I'm getting thisπŸ˜…: image Probably a combination of wrong user agend and api key... Since I don't have access to a Pint or XR this would help me very much!

TomasHubelbauer commented 4 years ago

I'll try when I have a chance, I'm also looking to port the unlock code. Thanks @beeradmoore

So I'll share my JSON ASAP within the next few days if you don't have an example by then.

As a side note, fuck this "private API misuse" bullshit. When will companies ever learn. πŸ˜–

COM8 commented 4 years ago

An other thing: @beeradmoore where do you get the device name from? Is this the Bluetooth device name or does the OW publish it's name without being unlocked? What happens if somebody changes the name of its OW?

TomasHubelbauer commented 4 years ago

I believe renaming the OW only affects the display in the app but is not actually written into the board, so the Bluetooth name always remains the same. But I'm not sure this is accurate.

beeradmoore commented 4 years ago

Can you/somebody else give an example JSON response from the API?

@COM8 , sure it looks something like this, same length, {"key":"XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"} This is the name from the bluetooth device. Serial number is different on the Pint to the device name. On my Plus they match.

It was not throwing "private IP blah blah" the other day when I was throwing random ID/Keys at it, so that's new πŸ˜‚.

But yes if you're reading this FM, block IPs of cellular phones that change on the regular, or block IPs of places with shared IP (eg. college), that'll work great and won't accidentally block anyone who just go their Pint/XR for Christmas πŸ‘

ocornu commented 4 years ago

I believe renaming the OW only affects the display in the app but is not actually written into the board, so the Bluetooth name always remains the same. But I'm not sure this is accurate.

That's right: they're two different things. I don't know where the custom OW name is stored exactly (within the board or on the network), but it's not in the app/device. I fix a lot of boards and, after i've connected to them, the custom name owner has chosen does appear on my app.

ocornu commented 4 years ago

It was not throwing "private IP blah blah" the other day when I was throwing random ID/Keys at it, so that's new joy.

Oh, so that's your fault! ;D

Zol-Tank commented 4 years ago

What worries me about this, is what happens if FM closes shop? Having the app worked tied to the company is a very worrisome development for a lot of reasons outside of ponewheel.

micHar commented 4 years ago

Amazing job reverse engineering that stuff!

Did anyone get a chance to find out how does the unlock key change in time? It is indeed cached in the app (I tried to connect to pint through official app with no Internet and it worked). I decompiled the official app and will see if I can find how it stores the key.

Anyway, the key has to be trusted by the firmware to unlock the board's BT communication. I don't think it's constant, that would be too easy (?), so I wonder what it might be. The only thing that comes to my mind atm is that it's some kind of One-Time-Password (HOTP / TOTP). The key and algorithm used to generate it needs to be written in the firmware, so that could be reverse engineered as well, I suppose.

This requires some knowledge out of my scope, I wonder if you guys have tried to work with someone who knows his way around the firmware?

beeradmoore commented 4 years ago

I just re-checked my Pint and both the key and activation code are both the same as they were in early December.

It's very likely that the key from the board is a value from using the serial as a hash. The alternative is its a second static value on the board which is the key, and FM have a giant lookup table containing serial and key.

micHar commented 4 years ago

I would rather bet on the first possibility tbh. That would mean that the algo for the hash is in the firmware and if we could reverse engineer that, we would be free from the API.

beeradmoore commented 4 years ago

I'd agree. It means that whatever FM do on their side is also very likely a hash.

Board serial --> Hash (ApiKey) -> Server hash -> Response (Key) -> Board -> Verification

Some other possibilities are the key from the server is not based on ApiKey which is sent to it, but instead ApiKey is just to validate the serial is correct and exists. And therefor key from the server is 100% based on serial. (eg, we don't need to do work on ApiKey to get Key)

Whatever server is doing to hash, it's most likely replicated on the board (unless they are just checking some verification bits, but I don't expect that to be the case). So I guess from a technical standpoint I wouldn't expect it to be too complex of an algorithm to make sure it can not only fit on the board but execute quickly.

I have limited knowledge in cryptography and low powered CPU models like found on the OW so I don't know what we should expect there.

beeradmoore commented 4 years ago

For a quick sanity check (and shooting fish in a barrel) I got my serial number, converted it to byte array and did a MD5 (correct length for ApiKey) and SHA1 (correct length for Key) hash on it. Wouldn't generate the same code.

Double checking this tread I can see that both the key directly from the board (ApiKey is just a subset of these values) and they key from the server both start with 098E56, and the final byte is also calculated by XORing all the previous data in the byte array. This is the same check that was done with the gemini firmware to calculate the last byte so I suspect some re-used code/hash/algorithms here.

public static byte GetVerificationByte(byte[] data)
{
    // ^ is a logcial exclusive OR operation
    // XOR

    byte verificationByte = 0;
    for (int i = 0; i < data.Length; ++i)
    {
        verificationByte = ((byte)(data[i] ^ verificationByte));
    }
    return verificationByte;
}

So the value that is sent to the server (ApiKey) is trimmed of this static data (first 3 bytes and final checksum/verification byte). But what is interesting is the key returned from the server already includes both the same 3 bytes and a valid checksum value on the end.

So to me this either means the server code is:

  1. Generating something based on the serial.
  2. Manually pre/appending known static values to something it hashed based on ApiKey.
micHar commented 4 years ago

The key is hex, right? The prepended part 098E56 is just 626262 in DEC, seems too specific to be random to me. This is a far shot, but I would say we don't look at any widely used hash algo like sha256. If I hashed something I would rather salt it first and then hash and here the prepended part seems to be specific standalone as hex.

Anyway, I will try to check what my key looks like when I'm free and maybe we could compare them and see if we find something interesting.