Skip to content

Instantly share code, notes, and snippets.

@magnusnordlander
Last active July 19, 2023 10:50
Show Gist options
  • Star 18 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save magnusnordlander/c8682fda2e15b813e5308624877cce59 to your computer and use it in GitHub Desktop.
Save magnusnordlander/c8682fda2e15b813e5308624877cce59 to your computer and use it in GitHub Desktop.
Vanmoof SA5 Certificate exporter

Requirements

  • node
  • @noble/ed25519
  • axios

How to use

  1. Update the last line of keypair-generator.js to use your Vanmoof account details.
  2. Run node keypair-generator.js > bikecredentials.txt
  3. Verify that bikecredentials.txt contains a private key ("Privkey"), and both a certificate and an ECU Serial for each SA5 bike in your account.
  4. Verify that the certificate(s) in bikecredentials.txt has an expiry 10 years in the future (previously this was 7 days)
  5. Save bikecredentials.txt somewhere safe. These files contain everything needed to communicate with your bike(s).
/*
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
const axios = require('axios');
const getCert = (async (email, password) => {
const ed = await import("@noble/ed25519");
const privKey = ed.utils.randomPrivateKey(); // Secure random private key
const pubKey = await ed.getPublicKeyAsync(privKey);
console.log("Privkey = " + Buffer.from(privKey).toString('base64'));
console.log("Pubkey = " + Buffer.from(pubKey).toString('base64'));
console.log("");
try {
const authResponse = await axios.post('https://my.vanmoof.com/api/v8/authenticate', null, {
headers: {
'Authorization': "Basic "+Buffer.from(email+':'+password).toString('base64'),
'Api-Key': "fcb38d47-f14b-30cf-843b-26283f6a5819",
'User-Agent': "VanMoof/20 CFNetwork/1404.0.5 Darwin/22.3.0",
}
});
const authToken = authResponse.data.token;
const appTokenResponse = await axios.get('https://api.vanmoof-api.com/v8/getApplicationToken', {
headers: {
'Authorization': "Bearer "+authToken,
'Api-Key': "fcb38d47-f14b-30cf-843b-26283f6a5819",
'User-Agent': "VanMoof/20 CFNetwork/1404.0.5 Darwin/22.3.0",
}
});
const appToken = appTokenResponse.data.token;
const bikeDataResponse = await axios.get('https://my.vanmoof.com/api/v8/getCustomerData?includeBikeDetails', {
headers: {
'Authorization': "Bearer "+authToken,
'Api-Key': "fcb38d47-f14b-30cf-843b-26283f6a5819",
'User-Agent': "VanMoof/20 CFNetwork/1404.0.5 Darwin/22.3.0",
}
});
for (const bikeIdx in bikeDataResponse.data.data.bikes) {
const bike = bikeDataResponse.data.data.bikes[bikeIdx];
console.log("Bike "+bike.name);
console.log("Bike ID: "+ bike.bikeId);
console.log("Frame number: "+ bike.frameNumber);
console.log("Frame serial: "+ bike.frameSerial);
if (bike.bleProfile === 'ELECTRIFIED_2022') {
console.log("Bike is an SA5");
console.log("ECU Serial: "+ bike.mainEcuSerial);
const certificateResponse = await axios.post('https://bikeapi.production.vanmoof.cloud/bikes/'+bike.bikeId+'/create_certificate', {
'public_key': Buffer.from(pubKey).toString('base64'),
}, {
headers: {
'Authorization': "Bearer "+appToken,
'User-Agent': "VanMoof/20 CFNetwork/1404.0.5 Darwin/22.3.0",
}
});
console.log("Certificate below:")
console.log("-----------");
console.log(certificateResponse.data);
console.log("-----------");
} else {
console.log("Not an SA5.")
}
}
console.log("");
console.log("");
} catch (error) {
console.log(error);
}
});
getCert("YOUR_VANMOOF_EMAIL", 'YOUR_VANMOOF_PASSWORD');
@Power2All
Copy link

Power2All commented Jul 18, 2023

@nivvle @magnusnordlander
The wording "failliet" in Dutch, is the same as "bankrupt".
It means they aren't able to pay their debt, and thus are insolvent, which is the same as being bankrupt.
You can still get out a bankruptcy though, there is no difference with being insolvent.
You can find the info how it's done in Netherlands here:
https://business.gov.nl/ending-your-business/financial-problems-and-bankruptcy/bankruptcy-procedure/

Here a translation from Dutch:

The (legal) person that is declared bankrupt is called bankrupt, other terms for the legal concept of bankruptcy are bankrupt, insolvency and pleaded.

Here the original Dutch wording for it:

De (rechts-)persoon die failliet wordt verklaard heet failliet, andere termen voor het wettelijke begrip faillissement zijn bankroet, insolventie en pleite.

Ontopic:
@magnusnordlander The person I was fixing this for, noticed his bike wasn't present at the dashboard, he will let me know to try it again :)

@gregmcd
Copy link

gregmcd commented Jul 18, 2023

Thank you so much for this, super helpful!

A messy (uncleaned ChatGPT translated) version of this code in Python that worked for me is below in case you, like me, didn't have Node installed.

import base64
import requests
import nacl.signing

def getCert(email, password):
    signing_key = nacl.signing.SigningKey.generate()  # Secure random private key
    verify_key = signing_key.verify_key  # Corresponding public key

    privKey = signing_key.encode(encoder=nacl.encoding.Base64Encoder)
    pubKey = verify_key.encode(encoder=nacl.encoding.Base64Encoder)

    print("Privkey = " + privKey.decode())
    print("Pubkey = " + pubKey.decode())

    try:
        headers = {
            'Authorization': "Basic " + base64.b64encode(f"{email}:{password}".encode()).decode(),
            'Api-Key': "fcb38d47-f14b-30cf-843b-26283f6a5819",
            'User-Agent': "VanMoof/20 CFNetwork/1404.0.5 Darwin/22.3.0",
        }

        authResponse = requests.post('https://my.vanmoof.com/api/v8/authenticate', headers=headers)
        authToken = authResponse.json()['token']

        headers['Authorization'] = "Bearer "+ authToken
        appTokenResponse = requests.get('https://api.vanmoof-api.com/v8/getApplicationToken', headers=headers)
        appToken = appTokenResponse.json()['token']

        bikeDataResponse = requests.get('https://my.vanmoof.com/api/v8/getCustomerData?includeBikeDetails', headers=headers)

        for bike in bikeDataResponse.json()['data']['bikes']:
            print("Bike "+bike['name'])
            print("Bike ID: "+ str(bike['bikeId']))
            print("Frame number: "+ bike['frameNumber'])
            print("Frame serial: "+ bike['frameSerial'])

            if bike['bleProfile'] == 'ELECTRIFIED_2022':
                print("Bike is an SA5")
                print("ECU Serial: "+ bike['mainEcuSerial'])

                headers['Authorization'] = "Bearer "+appToken
                certificateResponse = requests.post(
                    f'https://bikeapi.production.vanmoof.cloud/bikes/{bike["bikeId"]}/create_certificate',
                    headers=headers, 
                    json={'public_key': pubKey.decode()})

                print("Certificate below:")
                print("-----------")
                print(certificateResponse.json())
                print("-----------")
            else:
                print("Not an SA5.")
    except Exception as error:
        print(error)

getCert("YOUR_VANMOOF_EMAIL", 'YOUR_VANMOOF_PASSWORD')

@KurgerBing281
Copy link

KurgerBing281 commented Jul 19, 2023

@KurgerBing281 I'm guessing there might be something wrong with your public key? It depends on what error you're getting.

@magnusnordlander Yeah, that's probably it. I used ssh-keygen -t ed25519. Then applied awk '{ printf $2 }' my_public_key.txt, both with and without piping to base64. It was helpful to see your script apply base64. I always mess up my encryption. That's why I appreciated your approach, for an integrated solution, verified by others than myself.
curl mainly spat out errors when not using the --data-urlencode option, complaining about some malformed URL (probably the padding = characters in base64). However, subsequent attempts using the correct syntax/options gave no result at all. Just nothing. And an exit code of 0. Figured it might also be some server ddos protection, preventing too many attempts in a short timeframe. Alternatively, replacing production with test in the URL gave the same results (the IP address was also the same for both test and production, for what it's worth).

If you're using the curl calls, just make sure to save not just the certificate, but the private key and the ECU serial as well.

Yes, I repeatedly saved the bike details and the successful private/public keys.

In my bike details JSON some other API calls were mentioned by the way. With a little adjusting I could get some other hashes, that I saved just to be sure. I think it was replacing v8 by api/v8 in the URL.


About the translation issues: I only guessed that "failliet" translated to "bankrupt", couldn't come up with a better word. But I admit to not having it checked by Google Translate 🙂 (just thrilled that I finally got it working).


@gregmcd awesome! Being so focused on getting nodejs to work, it didn't even occur to me that python could be a great alternative. For old-fashioned people like me, that's a little less "exotic" than nodejs.

Also new to the whole GitHub thing (used to GitLab), so I missed the notifications at first. Nice to see this remains an active topic.

@magnusnordlander
Copy link
Author

@KurgerBing281 I've never gotten ssh-keygen to work to generate the keys. Known-good (to me) implementations are @noble/ed25519, ed25519swift and Google Tinkey (though I never got it to work with the CLI, just with code). I'm sure other implementations work as well, but those are the one I've tested.

@KurgerBing281
Copy link

@gregmcd The python script worked! ✔️
Just had to install pynacl (which is still imported as nacl) using pip
(off-topic: so I guess npm is to nodejs, what pip is to python).
Thanks for your work.


@magnusnordlander Somewhat reassuring I'm not the only one being baffled by ssh-keygen sometimes. Thanks for the other tips.

Off-topic: I noted ed25519swift is made available by one 'pebble8888' - made me remember my old Pebble watch 💔, and the subsequent Rebble-servers that replaced the offline official Pebble servers after fitbit killed the Pebble brand. Should the same thing happen to VanMoof, I hope some community will provide a similar service.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment