Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pint Support #109

Open
ghost opened this issue Dec 3, 2019 · 62 comments
Open

Pint Support #109

ghost opened this issue Dec 3, 2019 · 62 comments

Comments

@ghost
Copy link

ghost commented Dec 3, 2019

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
Copy link

I have the same problem. 🙂

@ghost
Copy link
Author

ghost commented Dec 4, 2019

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
Copy link
Author

ghost commented Dec 4, 2019

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
Copy link
Author

ghost commented Dec 4, 2019

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
Copy link

muellergit commented Dec 4, 2019 via email

@ghost
Copy link
Author

ghost commented Dec 4, 2019

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
Copy link

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
Copy link

muellergit commented Dec 4, 2019 via email

@ghost
Copy link
Author

ghost commented Dec 4, 2019

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

@tekkies
Copy link

tekkies commented Dec 5, 2019

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
Copy link
Author

ghost commented Dec 5, 2019

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
Copy link

tekkies commented Dec 5, 2019

@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
Copy link
Author

ghost commented Dec 5, 2019

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
Copy link

muellergit commented Dec 5, 2019 via email

@ghost
Copy link
Author

ghost commented Dec 5, 2019

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
Copy link
Author

ghost commented Dec 5, 2019

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
Copy link
Author

ghost commented Dec 5, 2019

So theres another thread over here for another app here, it doesnt look like they got as far though COM8/UWP-Onewheel#6

@ghost
Copy link
Author

ghost commented Dec 6, 2019

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
Copy link

beeradmoore commented Dec 7, 2019

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
Copy link
Author

ghost commented Dec 7, 2019

Ya my email is [email protected] and my discord is Nanoux #6524

@tekkies
Copy link

tekkies commented Dec 21, 2019

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
Copy link

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
Copy link
Author

ghost commented Dec 22, 2019

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

@tekkies
Copy link

tekkies commented Dec 24, 2019

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
Copy link
Collaborator

biell commented Dec 24, 2019

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
Copy link

tekkies commented Dec 24, 2019

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

@tekkies
Copy link

tekkies commented Dec 24, 2019

@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
Copy link

@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
Copy link
Author

ghost commented Dec 24, 2019

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.

@COM8
Copy link

COM8 commented Dec 27, 2019

@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
Copy link

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
Copy link

COM8 commented Dec 27, 2019

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
Copy link

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.

COM8 added a commit to COM8/UWP-Onewheel that referenced this issue Dec 27, 2019
@beeradmoore
Copy link

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
Copy link

ocornu commented Dec 27, 2019

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
Copy link

ocornu commented Dec 27, 2019

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
Copy link

Zol-Tank commented Mar 7, 2020

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
Copy link

micHar commented Apr 22, 2020

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
Copy link

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
Copy link

micHar commented Apr 22, 2020

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
Copy link

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
Copy link

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
Copy link

micHar commented Apr 22, 2020

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.

@Tbruno25
Copy link

Tbruno25 commented Aug 9, 2021

I know this is an old issue but I was hoping to see if anyone is aware whether the methed @beeradmoore outlines is still functional for retrieving the api key?

I converted the code to a small python script yet always receive the same response no matter the length of time --
{'message': 'Too much activations for today, please wait one day or contact support!'}

@beeradmoore
Copy link

@Tbruno25 , they totally block you after a few requests for the same token. I think your board might be blocked and they want you to call them to get a slap on the wrist.

Although the overall network protocol is being weird.

We launched this feature in OWCE on Jan 1st and within a week it stopped working. Even though we were spoofing absolutely everything possible FM found a way to detect and block it. The weird part is now when I launch a fresh official OW app with my Pint connected I get the same network requests which makes me thing nothing changed, however the response I get is something like {'result': 'success'}. No keys are returned. I don't know if this is because my Pint is blocked or if they are doing something else. I do have a new test device with a different Apple account and no OW app installed, if I get around to it I'll monitor the network and see if its all the same. If nothing is changed I should see the code returned.

What I believe Is probably happening is the keys are being backed up to the device cloud (iCloud in this instance) and we are not able to capture that on delete/reinstall. Maybe it can be dumped with some more advanced tools.

That is a lot of work to get something that may not prove to be much of anything. Something new is in the works to get the key though. More info at a later date.

@beeradmoore
Copy link

@Tbruno25 , I just went and did a request on my test device that I thought I had not installed OW app on (fresh device, fresh apple account), however the App Store download icon was a cloud so I must have at some point installed this. I can't confirm there is no cloud sync funny business going on here unfortunately. But here is my full header request and response.

Request header:

GET /wp-json/fm/v2/activation/{BOARD_SERIAL}?owType=pint&apiKey={API_KEY}&uuid={UUID_KEY}&fwVersion=5042&hwVersion=5300 HTTP/1.1
Host: app.onewheel.com
Accept: application/json
Content-Type: application/json
Connection: keep-alive
Cookie: AWSELB={SOME_AWS_TOKEN}; AWSELBCORS={SOME_AWS_TOKEN}
Accept-Language: en-au
Authorization: Basic Og==
Accept-Encoding: gzip
User-Agent: Onewheel/0 CFNetwork/1240.0.4 Darwin/20.6.0

BOARD_SERIAL: My board serial number
API_KEY: API key fetched from the board.
UUID_KEY: Key which is in the same format as a stackoverflow question here. So this is for firebase. The app does hit up firebaselogging/firelog API, I believe this is analytics logging. Multiple runs of delete/reinstall yield different IDs so I don't think this is tied to me in any way.
SOME_AWS_TOKEN - AWS token used to make sure when I hitup the Elastic Load Balancer that I end up at the same one again (I think..)

NOTE: Odd things happened running this with my main and my test device. My main device reported the HW version as 5300 (as above), my test device (iOS 12) reported my board as 5301. It says 5300 in board details. Other requests also reported it as a 5300. So yeah.. that's a thing.

Response header:

HTTP/1.1 200 OK
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Expose-Headers: X-WP-Total, X-WP-TotalPages
Allow: GET
Content-Type: application/json; charset=UTF-8
Date: Wed, 11 Aug 2021 05:23:15 GMT
Link: <https://app.onewheel.com/wp-json/>; rel="https://api.w.org/"
Server: Apache
Vary: User-Agent
X-Content-Type-Options: nosniff
X-Robots-Tag: noindex
Content-Length: 20
Connection: keep-alive

Response body:

{
    "result": "Success"
}

So the server never returned my token in activation. So either:
A) It was in iCloud and the app re-install synced it back.
B) It is no longer required, which goes against everything we see OWCE behaviour doing when we disable it in the app.
C) there is a new local activation handshake (but then why the online activation request?)

I still lean towards A, but I have no idea how to prevent this sync (on iOS or Android) so if anyone has any tips aside from completely wipe my phone.

@TomasHubelbauer
Copy link

@beeradmoore would a burner Apple ID work?

@beeradmoore
Copy link

beeradmoore commented Aug 11, 2021

Yeah, but I'm le tired. It also means I gotta spend another $8 on Charles Proxy.

EDIT: Looking to see if its possible to get BT passthrough to an Android Emulator.

@TomasHubelbauer
Copy link

I wish I knew how to fix the former, could use the solution for myself, too. But at least I can help with the latter. Do you have PayPal or would you consider setting up Github Sponsors?

@beeradmoore
Copy link

Appreciate it, but I don't want to take any money for OW endeavours. :)

I just did an encrypted backup of my test phone and busted it open with mvt and found this file.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>OnewheelsList</key>
	<data>
	YnBsaX{...REDACTED...}HQ==
	</data>
</dict>
</plist>

Did a base64 decode and found it to be a binary plist file, and inside it is my device serial and key (one returned from the server) as well as a GUID which I don't know is for.

@beeradmoore
Copy link

beeradmoore commented Aug 11, 2021

Below I did the same steps again and again. That is full backup, decrypt backup, check result files for OnewheelsList.

First up was after I deleted the app. No OnewheelsList found. This was to make sure no additional data was hanging around.

Installed the app, but didn't open it. OnewheelsList was not found (I am unsure of the other files listed below exited or not).

After opening the app but not connecting to my board OnewheelsList was found but is empty.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>OnewheelsList</key>
	<data>
	</data>
</dict>
</plist>

If this is empty here I assume its not doing a cloud fetch on launch.

Here is a list of all Onewheel/FutureMotion files extracted from a backup

         INFO     [mvt.ios.decrypt] Decrypted file Documents/Credentials.plist [AppDomain-com.futuremotion.onewheel] to                                                          
                  Backup_4_decrypted/c4/c491a4c0a182372413d9d832167f8681892685b1                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Documents/GPSCurrentTrack.plist [AppDomain-com.futuremotion.onewheel] to                                                      
                  Backup_4_decrypted/69/69f6dd8f72f1cbbd2292dd2946a27cce0e708533                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Documents/Onewheels.plist [AppDomain-com.futuremotion.onewheel] to                                                            
                  Backup_4_decrypted/c5/c5d9eb2cbb127781d5fa8452e85c12e07b1c5d0b                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Application Support/Google/FIRApp/FIREBASE_DIAGNOSTICS_HEARTBEAT_DATE [AppDomain-com.futuremotion.onewheel] to        
                  Backup_4_decrypted/99/9907c9655e9bee865cc77d15415802f5d6847e0a                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Application Support/Google/FIRApp/HEARTBEAT_INFO_STORAGE [AppDomain-com.futuremotion.onewheel] to                     
                  Backup_4_decrypted/cb/cb317a00a492c52ef2c1e9f4ecbc4227fbac9175                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Application Support/Google/FirebaseMessaging/rmq2.sqlite [AppDomain-com.futuremotion.onewheel] to                     
                  Backup_4_decrypted/da/dabdf9350d8e7ff304313e2125fd5b777f91b386                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Application Support/com.crashlytics/CLSUserDefaults.plist [AppDomain-com.futuremotion.onewheel] to                    
                  Backup_4_decrypted/f4/f4b841d0f009a9ba234d6e4b1b892875b5ac4605                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Cookies/Cookies.binarycookies [AppDomain-com.futuremotion.onewheel] to                                                
                  Backup_4_decrypted/7b/7b5c3e103d1e93449d86b10b3ee8c88a8349ceff                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/APMAnalyticsSuiteName.plist [AppDomain-com.futuremotion.onewheel] to                                      
                  Backup_4_decrypted/a2/a2f00162b8a2135bc2e227474d80078a74e34da2                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/APMExperimentSuiteName.plist [AppDomain-com.futuremotion.onewheel] to                                     
                  Backup_4_decrypted/cf/cf38899f1ecb4b5503f3c4a46f840f1364ddc0fe                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/com.firebase.FIRInstallations.plist [AppDomain-com.futuremotion.onewheel] to                              
                  Backup_4_decrypted/1f/1f4eb9a523ecaadff86894394a3391fe9909b35c                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/com.futuremotion.onewheel.plist [AppDomain-com.futuremotion.onewheel] to                                  
                  Backup_4_decrypted/4f/4f88f5bafb85ee557aac155883ad5e89a32d3551                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/com.google.gmp.measurement.monitor.plist [AppDomain-com.futuremotion.onewheel] to                         
                  Backup_4_decrypted/ed/edc2026840990d1e3d2fa66df3cbb453e4d2cb4f                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/com.google.gmp.measurement.plist [AppDomain-com.futuremotion.onewheel] to                                 
                  Backup_4_decrypted/d2/d2aa5e6b0d8fc4e0b1270d4a8fae19580ef3ddc4                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/Preferences/group.onewheel.plist [AppDomainGroup-group.onewheel] to                                                   
                  Backup_4_decrypted/d1/d12256df64ba13248dc5ba0518b0ca17da2b768e          
         INFO     [mvt.ios.decrypt] Decrypted file Library/UserNotifications/com.futuremotion.onewheel/Categories.plist [HomeDomain] to                                          
                  Backup_4_decrypted/e8/e857db934cda56d7fdb030e9453a8777606afd2a                                                                                                 
         INFO     [mvt.ios.decrypt] Decrypted file Library/UserNotifications/com.futuremotion.onewheel/PushRegistration.plist [HomeDomain] to                                    
                  Backup_4_decrypted/5b/5b1d08e3fef63a34b7721f1bb378db26f7ae8f1e

The file we keep checking is Documents/Onewheels.plist.

Went through setup, detected my board but did not connect to it. OnewheelsList data is still empty.

Airplane mode enabled. Connected to board and got the "Online activation failed - Please ensure to have internet connection or try again later"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>OnewheelsList</key>
	<data>
	YnBsaX{REDACTED}AAAeo=
	</data>
</dict>
</plist>

OnewheelsList now has data!

Buuuut.... it is missing the value that would be where the key is. The UUID I mentioned earlier is the same.

Turn off airplane mode. Connect to board. OnewheelsList now contains the data it originally did.

Looking into some more network connections again I can see that UUID_KEY mentioned above was returned from a fcmtoken.googleapis.com/register.

During the activation stage there was also a request it inappcheck.itunes.apple.com, but blocking on my network did not stop this field loading correctly.

Last attempt for the night I blocked:

  • device-provisioning.googleapis.com
  • valid.apple.com
  • valid-apple.g.aaplimg.com
  • firebaselogging-pa.googleapis.com
  • fcmtoken.googleapis.com
  • firebaseinstallations.googleapis.com

And now I got an activation error. So I think from this I would say something here is required for activation.

EDIT 1: I went and unblocked those domains one by one and I got a successful activation after I unblocked fcmtoken.googleapis.com. After re-blocking it I got a successful activation again. So it must be some combination of these. firebaseinstallations.googleapis.com appears to be another part of this activation system. With these blocked the activation endpoint is never called.

With just firebaseinstallations.googleapis.com and inappcheck.itunes.apple.com blocked I am not able to activate. Unblocking the former it now activates successfully. From this I would say if the key is stored in the cloud anywhere it is in the google cloud, not iCloud.

@davemcphee
Copy link

Totally wild guess, based on the APIs and @beeradmoore findings:

the /wp-json/fm/v2/activation endpoint takes your details, but instead of sending back the required secret, it creates a firebase cloud messaging topic using your Serial / key, and FCM pushes the secret to your device / OneWheel app

The UUID_KEY is your (FCM) device registration, and is mapped to a specific board, so eg.: I buy your board, I start the FM OW App, it hits the activation endpoint, it creates a new UUID_KEY in FCM, which gets mapped to the boards serial / api key, and now the secret gets pushed to my phone instead of yours.

@micHar
Copy link

micHar commented Aug 11, 2021

I would also say it's just pushing the key to the device. I've seen this way of obscuring communication before.

It means that the token will only be sent to the app with a specific app id and signed with a specific key (fcm ensures that) so there isn't an easy way to start receiving these push msgs in a 3rd party app, I think :/.

Best you can do is to extract the key from shared prefs or smth on android and the plist file on ios and put it in 3rd party app, which isn't amazing ux ;)

@beeradmoore
Copy link

I don't know enough about FCM to know if it pushes via the Apple Push Notification Service (APNS). I would assume so because I see it sending my APNS device ID in some requests going to FCM. I also don't know if I say no to push notifications if that disables APNS completely. Rejecting push notifications from the prompt still allowed the board to register.

If it is still allowed to come through in the background I think it's part of the socket connections the device makes back to Apple services, I am not sure how to capture them.

I would agree that this is how they are obfuscating the token. Hope is not lost though. There are still a few tricks left in the bucket.

As for the situation as a whole, this quote via twitter rings so very true.

I did not expect this kind of blue team / red team stuff from a bunch of californian surfer dudes :/

@micHar
Copy link

micHar commented Aug 12, 2021

FCM uses APNS but it doesn't really matter, app can use it directly for the same effect. As for background pushes, afaik it can be sent in background without explicit permission, although if you disable background processing in system settings it should probably not go through then.

iOS and Android are managing socket connection for their push services but this is something baked into system on ios and into Google play services (or similar from huawei etc) on android. I'm pretty sure they use cert pinning for that (basic security of a critical system function) so MITM like Charles proxy is not gonna help too much.

I wonder what other ideas you have :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests