BMW vehicles offer limited control and telemetry through the MyBMW app. This library uses the private APIs to enable scipting and integrations with third party tools.
npm install -g https://github.com/colinbendell/bmw
- Configuration #authentication
- Use the
bmw
CLI
Authentication uses the MyBMW account credentials. It can be passed through to the library using one of three ways:
-
environment variables
BMW_EMAIL
,BMW_PASSWORD
andBMW_GEO
-
config file located
~/.bmw
in the form of:[default] email[email protected] password=p@$$w0rd geo=na
-
pass the values directly into the constructor
new BMWClientAPI(username, password, geo)
The valid
geo
values are:na
(North America),cn
(China) androw
(Rest of World).
NB:
na
androw
has been tested so far
The ./bmw
provides a number of commands to invoke the APIs through a shell script. Use bmw --help
for expanded help details.
The available commands include:
bmw login
to test the auth credentialsbmw charge [vin]
to start charging the carbmw climate [vin]
to start charging the carbmw lock [vin]
to lock the vehiclebmw unlock [vin]
to unlock a specific vehiclebmw flash [vin]
to flash the lightsbmw honk [vin]
to unlock the vehiclebmw list
list the vehicles associated with the accountbmw info [vin]
current status info of the vehiclebmw status [vin]
build and configuration info of the vehiclebmw trips [vin]
list trip information for a given monthbmw charging [vin]
log of the charges for the vehicle
The
[vin]
is optional in all cases. If absent, all vehicles are used.
Protip: You can use parts of the model name instead of the
[vin]
. Eg:bmw status iX
There are three main components to the library:
src/bmw-api.js
- API wrapper library that manages authentication (see #auth)src/bmw.js
- the main business logic that wraps over the api callssrc/bmw-cli.js
- a convenience CLI for scripting and automation
Extracting the current set of APIs is a bit of a challenge because the MyBMW app is built with Flutter. While Flutter is a convenient dev environment, it doesn't use the OS provided network libraries like nsURLSession or HttpClient. This would make it easier to trace the Network access and the API calls using a standard MitM tooling approaches (proxy + root CA). Instead, Flutter apps roll their own network stack which requires a lot more work to intercept the applications network calls.
The simplest approach is:
- Install Android Studio
- Create a Virtual Device in the VDM (Virtual Device Manager). Currently My BMW needs Android 31+
- Download the latest My BMW APK (either copy the installed APK from the play store install, or download from apkpure or other sources)
- Use
adb root
thenadb pull /data/app/<hash>/de.bmw.connected.mobile20.na-<hash>/base.apk de.bmw.connected.mobile20.na.apk
- Downlaod from apkmirror.com or apkpure.com
- Use
- Install Frida:
pip3 install frida-tools
- Use
Gadget
to wrap the APK:- Installing objection:
pip3 install objection
- You might need to add
/build-tools
to yourPATH
and install apktool (brew install apktool
) - Run
objection patchapk -s de.bmw.connected.mobile20.na.apk
- Install the patched apk to the device
- Installing objection:
- Install Wireguard on the android device
- Available on the Play Store or F-droid
- Install mitmproxy:
brew install mitmproxy
- Start mitm in wireguard mode:
mitmdump --mode wireguard --showhost --flow-detail 3 cocoapi.bmwgroup.us
- Install the wireguard profile as directed in the output
- NB: mitm with wireguard in docker on mac doesn't work because of the virtualized network stack. Best to run it in a terminal or use an rpi
- NB: current version of mitm assumes the 10.0.0/24 network is availble. Make sure your current network is on a different subnet
- Start mitm in wireguard mode:
- Download the Frida script as per the Flutter instructions
- Download disable-flutter-tls.js or for the more adventurous you can use the more comprehensive version
- Intercept traffic
- Connect to the wireguard server
Not required, but to test that browser interception is working, visit http://mitm.it and install the rootCA using the instructions)
- Launch the bmw app on the VMD. It will pause until you run frida
- on the local terminal run frida with gadget:
frida -U gadget -l disable-flutter-tls.js
- Connect to the wireguard server
The BMW app suffers from Conway's Law:
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.
This is apparent in the way that the authentication system is bifurcated across 3 geos (na
, cn
and row
) as well as the way it uses many different application paths for similar but discrete functions of the same component (eg: /eadrax-crccs/v1/vehicles/
vs /eadrax-chs/v2/charging-sessions
vs /eadrax-cps/v2/vehicles
).
Worse yet is the inconsistent use of ?
query parameters vs /
url path parameters, json body parameters and http headers. Take the VIN parameter as an example: for /charging-statistics?vin=
it is a query parameter, for /vehicles/${vin}/start-charging
and more bizarely for /vehicles/state
it needs to be an http header bmw-vin:
.
Other oddities to be aware of:
-
The
?date=
parameter is onlyyyyy-dd
formatted and not a real date for/v1/vehicles/sustainability/trips/history
-
The
?timezone=
is also necessary because the ?date= field isn't a proper ISO8701 formatted date which would have provided the timezone relative offset -
The
?date=
parameter must be GMT relative with millisecond precision for/v2/charging-sessions
-
But only the month is actually used in the
?date=
parameter as all sessions in that month will be provided -
Unless of course if you add
?include_date_picker=true
which will discregard the?date=
field and use the current month instead. There is no real value for the extra json values so just use?include_date_picker=false
. -
Some APIs require a Global Catalog ID HTTP Header. Use
x-gcid: ...
for/v1/vehicles/sustainability/trips/history
andbmw-gcis: ...
for/v2/recalls
. Why the inconsistency? What does this do? -
The GCID values appear to matter. See the sourcecode for specific values for each API.
-
Some API teams clearly never read the JSON spec and redundantly annotate
date/time
fields withtimeUnit
values. eg:"start": { "time": "2022-12-23T03:44:42Z", "timeUnit": "ISO8601" },
instead of simply"start": "2022-12-23T03:44:42Z"
. -
Sending commands uses a two step pattern of creating a POST action and then querying the end point. For example
POST /v3/presentation/remote-commands/...
thenGET /v3/presentation/remote-commands/eventStatus?eventId=...
-
There are different Remote Command endpoints
/eadrax-vrccs/v3/presentation/remote-commands
,/eadrax-vrccs/v1/vehicles
,/eadrax-r360/v1/event/execute
,/eadrax-r360/v1/vehicle/addresses
, etc -
The mix of application paths
eadrax-vrccs
andeadrax-vrccs
for the same set of remote commands is another example of different engineering teams with split ownership -
auth is done with Microsoft's API Management Open Product (hence the
ocp-apim-subscription-key
in the oauth dance) -
There are multiple session tokens used
bmw-session-id
for most api contexts, andGCDMSSO
Cookie for auth status. -
This is in addition to valid
Authorization: Bearer
oauth api tokens. -
It's unclear why bmw requires a session context when they provide oauth keys. I suspect that there are 3 different infrastructur teams at play managing server affinity, authorization and other contexts.
-
Many of the APIs also do content-negotiation through the use of HTTP headers. For example
bmw-units-preferences: d=KM;v=L
,country: uk
,24-hour-format: true
. -
Despite doing conneg with these HTTP headres, the response is missing
Vary:
to communicate the variances of the content -
bmw-correlation-id
andx-correlation-id
appear to be OTP Spans for tracing. It's kind of silly really to have the same header twice -
pagination sometimes uses
?offset=
+?limit=
while other times uses?next_token=
andmax_results=
. (see:/eadrax-suscs/v1/vehicles/sustainability/trips/history
and/eadrax-chs/v2/charging-sessions
respectively) -
naming conventions are very inconsistent. sometimes it's snake_case othertimes it's camel_case!
-
Most of the apis behave like apis. However, the Charging Sessions API is a for-purpose UX api. (
/eadrax-chs/v2/charging-sessions
). ThestartDate
is pre-formated to some version of US date/time rather than ISO8601 (sometimes it says 'yesterday' other times it will have a mm/dd/yyyy formatted date) -
the
timelineItems
array is a litteral json version of html with styling:{ "body": {"actionType": "CHARGING_TYPE_UNSPECIFIED", "isEnabled": true, "subtitle": "Saturday 16:36", "title": "Plugged in"}, "hasDivider": false, "leading": { "alignment": "CENTER", "backgroundColor": "TRANSPARENT", "backgroundOpacity": 1, "borderColor": "CONTENT_ACCENT", "borderOpacity": 1, "bottomLineColor": "PRIMARY", "bottomLineOpacity": 1, "color": "PRIMARY", "iconCode": 59692, "opacity": 1, "topLineColor": "TRANSPARENT", "topLineOpacity": 1 }, "trailing": {"actionType": "CHARGING_TYPE_UNSPECIFIED", "iconCode": 59694, "text": "57%"} },
-
dns for
cocoapi.bmwgroup.us
is unreliable and sometimes times-out. We need error capture and retry logic to manage this