tesla-fleet-api 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +32 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +95 -0
- package/dist/bluetooth/transport.d.mts +95 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/charging.cjs +53 -0
- package/dist/charging.d.cts +31 -0
- package/dist/charging.d.mts +30 -0
- package/dist/charging.mjs +53 -0
- package/dist/commands.cjs +1797 -0
- package/dist/commands.d.cts +881 -0
- package/dist/commands.d.mts +881 -0
- package/dist/commands.mjs +1791 -0
- package/dist/const.cjs +322 -0
- package/dist/const.d.cts +308 -0
- package/dist/const.d.mts +308 -0
- package/dist/const.mjs +282 -0
- package/dist/energy.cjs +142 -0
- package/dist/energy.d.cts +101 -0
- package/dist/energy.d.mts +100 -0
- package/dist/energy.mjs +142 -0
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +457 -0
- package/dist/energyspecific.d.cts +258 -0
- package/dist/energyspecific.d.mts +258 -0
- package/dist/energyspecific.mjs +451 -0
- package/dist/exceptions.cjs +1488 -0
- package/dist/exceptions.d.cts +1143 -0
- package/dist/exceptions.d.mts +1143 -0
- package/dist/exceptions.mjs +1325 -0
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +287 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +27 -0
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/partner.cjs +32 -0
- package/dist/partner.d.cts +24 -0
- package/dist/partner.d.mts +23 -0
- package/dist/partner.mjs +32 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -0
- package/dist/router.d.mts +3 -0
- package/dist/router.mjs +3 -0
- package/dist/signing/crypto.cjs +59 -0
- package/dist/signing/crypto.mjs +52 -0
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +186 -0
- package/dist/signing/session.d.cts +106 -0
- package/dist/signing/session.d.mts +106 -0
- package/dist/signing/session.mjs +184 -0
- package/dist/tariff.cjs +319 -0
- package/dist/tariff.d.cts +40 -0
- package/dist/tariff.d.mts +40 -0
- package/dist/tariff.mjs +317 -0
- package/dist/teslafleetapi.cjs +326 -0
- package/dist/teslafleetapi.d.cts +160 -0
- package/dist/teslafleetapi.d.mts +160 -0
- package/dist/teslafleetapi.mjs +326 -0
- package/dist/teslemetry.cjs +183 -0
- package/dist/teslemetry.d.cts +113 -0
- package/dist/teslemetry.d.mts +113 -0
- package/dist/teslemetry.mjs +181 -0
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +71 -0
- package/dist/tessie.d.cts +69 -0
- package/dist/tessie.d.mts +69 -0
- package/dist/tessie.mjs +71 -0
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/commands.d.cts +28 -0
- package/dist/types/commands.d.mts +28 -0
- package/dist/types/live_status.d.cts +30 -0
- package/dist/types/live_status.d.mts +30 -0
- package/dist/types/products.d.cts +58 -0
- package/dist/types/products.d.mts +58 -0
- package/dist/types/responses.d.cts +13 -0
- package/dist/types/responses.d.mts +13 -0
- package/dist/types/site_info.d.cts +154 -0
- package/dist/types/site_info.d.mts +154 -0
- package/dist/types/vehicle.d.cts +33 -0
- package/dist/types/vehicle.d.mts +33 -0
- package/dist/types/vehicle_data.d.cts +278 -0
- package/dist/types/vehicle_data.d.mts +278 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +9 -0
- package/dist/types.d.mts +9 -0
- package/dist/types.mjs +1 -0
- package/dist/user.cjs +28 -0
- package/dist/user.d.cts +17 -0
- package/dist/user.d.mts +16 -0
- package/dist/user.mjs +28 -0
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +847 -0
- package/dist/vehicle.d.cts +566 -0
- package/dist/vehicle.d.mts +565 -0
- package/dist/vehicle.mjs +847 -0
- package/dist/vehiclesigned.cjs +35 -0
- package/dist/vehiclesigned.d.cts +23 -0
- package/dist/vehiclesigned.d.mts +22 -0
- package/dist/vehiclesigned.mjs +35 -0
- package/dist/vehiclespecific.cjs +715 -0
- package/dist/vehiclespecific.d.cts +497 -0
- package/dist/vehiclespecific.d.mts +496 -0
- package/dist/vehiclespecific.mjs +715 -0
- package/package.json +178 -38
- package/dist/charging.d.ts +0 -30
- package/dist/charging.js +0 -51
- package/dist/charging.js.map +0 -1
- package/dist/commands.d.ts +0 -352
- package/dist/commands.js +0 -875
- package/dist/commands.js.map +0 -1
- package/dist/energy.d.ts +0 -94
- package/dist/energy.js +0 -125
- package/dist/energy.js.map +0 -1
- package/dist/energyspecific.d.ts +0 -77
- package/dist/energyspecific.js +0 -97
- package/dist/energyspecific.js.map +0 -1
- package/dist/helpers.d.ts +0 -2
- package/dist/helpers.js +0 -10
- package/dist/helpers.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/partner.d.ts +0 -23
- package/dist/partner.js +0 -31
- package/dist/partner.js.map +0 -1
- package/dist/signing/crypto.d.ts +0 -10
- package/dist/signing/crypto.js +0 -26
- package/dist/signing/crypto.js.map +0 -1
- package/dist/signing/errors.d.ts +0 -29
- package/dist/signing/errors.js +0 -40
- package/dist/signing/errors.js.map +0 -1
- package/dist/signing/session.d.ts +0 -69
- package/dist/signing/session.js +0 -102
- package/dist/signing/session.js.map +0 -1
- package/dist/tariff.d.ts +0 -29
- package/dist/tariff.js +0 -302
- package/dist/tariff.js.map +0 -1
- package/dist/teslafleetapi.d.ts +0 -58
- package/dist/teslafleetapi.js +0 -120
- package/dist/teslafleetapi.js.map +0 -1
- package/dist/teslemetry.d.ts +0 -29
- package/dist/teslemetry.js +0 -39
- package/dist/teslemetry.js.map +0 -1
- package/dist/tessie.d.ts +0 -23
- package/dist/tessie.js +0 -28
- package/dist/tessie.js.map +0 -1
- package/dist/types/commands.d.ts +0 -44
- package/dist/types/commands.js +0 -11
- package/dist/types/commands.js.map +0 -1
- package/dist/types/live_status.d.ts +0 -26
- package/dist/types/live_status.js +0 -2
- package/dist/types/live_status.js.map +0 -1
- package/dist/types/products.d.ts +0 -54
- package/dist/types/products.js +0 -2
- package/dist/types/products.js.map +0 -1
- package/dist/types/responses.d.ts +0 -5
- package/dist/types/responses.js +0 -2
- package/dist/types/responses.js.map +0 -1
- package/dist/types/site_info.d.ts +0 -150
- package/dist/types/site_info.js +0 -2
- package/dist/types/site_info.js.map +0 -1
- package/dist/types/vehicle.d.ts +0 -23
- package/dist/types/vehicle.js +0 -10
- package/dist/types/vehicle.js.map +0 -1
- package/dist/types/vehicle_data.d.ts +0 -274
- package/dist/types/vehicle_data.js +0 -2
- package/dist/types/vehicle_data.js.map +0 -1
- package/dist/user.d.ts +0 -16
- package/dist/user.js +0 -26
- package/dist/user.js.map +0 -1
- package/dist/vehicle.d.ts +0 -548
- package/dist/vehicle.js +0 -774
- package/dist/vehicle.js.map +0 -1
- package/dist/vehiclesigned.d.ts +0 -13
- package/dist/vehiclesigned.js +0 -19
- package/dist/vehiclesigned.js.map +0 -1
- package/dist/vehiclespecific.d.ts +0 -432
- package/dist/vehiclespecific.js +0 -601
- package/dist/vehiclespecific.js.map +0 -1
- package/src/charging.ts +0 -66
- package/src/commands.ts +0 -1134
- package/src/energy.ts +0 -217
- package/src/energyspecific.ts +0 -116
- package/src/helpers.ts +0 -11
- package/src/index.ts +0 -21
- package/src/partner.ts +0 -37
- package/src/signing/crypto.ts +0 -29
- package/src/signing/errors.ts +0 -50
- package/src/signing/session.ts +0 -132
- package/src/tariff.ts +0 -375
- package/src/teslafleetapi.ts +0 -182
- package/src/teslemetry.ts +0 -54
- package/src/tessie.ts +0 -42
- package/src/types/commands.ts +0 -62
- package/src/types/live_status.ts +0 -27
- package/src/types/products.ts +0 -61
- package/src/types/responses.ts +0 -1
- package/src/types/site_info.ts +0 -159
- package/src/types/vehicle.ts +0 -33
- package/src/types/vehicle_data.ts +0 -286
- package/src/user.ts +0 -35
- package/src/vehicle.ts +0 -886
- package/src/vehiclesigned.ts +0 -20
- package/src/vehiclespecific.ts +0 -818
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Domain } from "@teslemetry/tesla-protocol/command/universal_message";
|
|
2
|
+
import { AESGCMPersonalizedSignatureData, HMACPersonalizedSignatureData, SessionInfo } from "@teslemetry/tesla-protocol/command/signatures";
|
|
3
|
+
/** Key material derived (but not yet trusted/committed) for a candidate vehicle public key. */
|
|
4
|
+
export interface SessionKeys {
|
|
5
|
+
sharedKey: Buffer;
|
|
6
|
+
hmacKey: Buffer;
|
|
7
|
+
sessionInfoKey: Buffer;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Per-domain (VCSEC / Infotainment) signed-command session: tracks the
|
|
11
|
+
* anti-replay counter/epoch handed out by the vehicle's handshake reply and
|
|
12
|
+
* the HMAC keys derived from it.
|
|
13
|
+
*
|
|
14
|
+
* The vehicle's key is trusted once, at first contact, then pinned for the life
|
|
15
|
+
* of the session, as in Tesla's Go SDK. ECDH is symmetric, so anyone who saw the
|
|
16
|
+
* client's public key can tag a `SessionInfo` naming a key of their own: a tag
|
|
17
|
+
* proves nothing about a key derived from the same reply. `trustedKeys` is the
|
|
18
|
+
* only way a reply reaches key material, so once pinned it is verified with the
|
|
19
|
+
* stored keys, and nothing clears the pin. A re-keyed vehicle needs a new instance.
|
|
20
|
+
*
|
|
21
|
+
* First contact itself is trust on first use. Deriving keys (`keysFor`) is still
|
|
22
|
+
* split from committing them (`commit`), so a reply that fails its tag or carries
|
|
23
|
+
* a whitelist rejection pins nothing. See `Commands.validate_msg`.
|
|
24
|
+
*
|
|
25
|
+
* A `Session` is not itself thread-safe; `withLock` serializes the
|
|
26
|
+
* message-build step (which mutates `counter`) so concurrent commands on the
|
|
27
|
+
* same domain cannot race on the same counter value.
|
|
28
|
+
*
|
|
29
|
+
* The derived keys are native private fields, read through accessors, so a
|
|
30
|
+
* `JSON.stringify` or structured log of a session cannot reach them.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Session {
|
|
33
|
+
#private;
|
|
34
|
+
private readonly deriveSharedKey;
|
|
35
|
+
readonly domain: Domain;
|
|
36
|
+
counter: number;
|
|
37
|
+
epoch?: Buffer;
|
|
38
|
+
delta?: number;
|
|
39
|
+
/** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
|
|
40
|
+
private lastAuthenticatedClockTime?;
|
|
41
|
+
/** `(requestHash, counter)` pairs seen in this epoch, oldest first. */
|
|
42
|
+
private responseCounters;
|
|
43
|
+
private queue;
|
|
44
|
+
constructor(domain: Domain, deriveSharedKey: (vehiclePublicKey: Buffer) => Buffer);
|
|
45
|
+
/** The vehicle key pinned at first contact. There is deliberately no setter. */
|
|
46
|
+
get publicKey(): Buffer | undefined;
|
|
47
|
+
get sharedKey(): Buffer | undefined;
|
|
48
|
+
get hmacKey(): Buffer | undefined;
|
|
49
|
+
get sessionInfoKey(): Buffer | undefined;
|
|
50
|
+
/** Whether the handshake has completed and commands can be signed. */
|
|
51
|
+
get ready(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Derive the shared key and its two HMAC sub-keys for a candidate vehicle
|
|
54
|
+
* public key, without touching any session state. Pure and side-effect
|
|
55
|
+
* free so a caller can verify authenticity first and discard the result
|
|
56
|
+
* if verification fails. Verifying a reply goes through `trustedKeys`.
|
|
57
|
+
*/
|
|
58
|
+
keysFor(vehiclePublicKey: Buffer): SessionKeys;
|
|
59
|
+
/**
|
|
60
|
+
* The keys to verify a reply naming `vehiclePublicKey` with: derived from it
|
|
61
|
+
* at first contact, and the stored keys from then on, never derived again.
|
|
62
|
+
* @throws SessionInfoAuthenticationFault when the session is pinned to another key.
|
|
63
|
+
*/
|
|
64
|
+
trustedKeys(vehiclePublicKey: Buffer): SessionKeys;
|
|
65
|
+
/**
|
|
66
|
+
* Commit an already-authenticated `SessionInfo` plus its `trustedKeys`
|
|
67
|
+
* result. Keys are adopted at first contact only: after that, `keys` is
|
|
68
|
+
* ignored and a different public key is refused. Per the vehicle-command
|
|
69
|
+
* protocol, the anti-replay counter must never roll back within the same
|
|
70
|
+
* epoch, and a clock time that regresses within the same epoch marks the
|
|
71
|
+
* reply as stale/replayed - returns `false` (without mutating any state) in
|
|
72
|
+
* that case so the caller can reject it.
|
|
73
|
+
* @throws SessionInfoAuthenticationFault, changing nothing, for a different public key, or a clock or counter that could not sign a single command.
|
|
74
|
+
*/
|
|
75
|
+
commit(sessionInfo: SessionInfo, keys: SessionKeys): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Record an authenticated response's counter for the request it answers.
|
|
78
|
+
* @returns `false` if that pair was already seen, marking the response a replay.
|
|
79
|
+
*/
|
|
80
|
+
record_response_counter(requestHash: Uint8Array, counter: number): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Advance the anti-replay counter for a command valid for `lifetimeSeconds`,
|
|
83
|
+
* corrected for clock drift against the vehicle (`delta`). The clock keeps
|
|
84
|
+
* running after `commit`, so its limit is checked here too; a session past it
|
|
85
|
+
* recovers when the vehicle starts a new epoch.
|
|
86
|
+
* @throws SessionInfoAuthenticationFault, leaving the counter alone, when either value would overflow its wire field.
|
|
87
|
+
*/
|
|
88
|
+
private nextSignature;
|
|
89
|
+
/**
|
|
90
|
+
* Fresh signature metadata for an HMAC-personalized command, which expires
|
|
91
|
+
* 10 seconds from now.
|
|
92
|
+
*/
|
|
93
|
+
hmacPersonalized(): HMACPersonalizedSignatureData;
|
|
94
|
+
/**
|
|
95
|
+
* Fresh signature metadata for an AES-GCM-personalized command, with a
|
|
96
|
+
* random nonce, which expires 30 seconds from now.
|
|
97
|
+
*/
|
|
98
|
+
aesGcmPersonalized(): AESGCMPersonalizedSignatureData;
|
|
99
|
+
/**
|
|
100
|
+
* Run `fn` with exclusive access to this session, queuing behind any
|
|
101
|
+
* in-flight caller. Not reentrant: `fn` must not call `withLock` on this
|
|
102
|
+
* session. Infotainment holds it for the message build only, and VCSEC for
|
|
103
|
+
* the whole dispatch (see `Commands.dispatch`).
|
|
104
|
+
*/
|
|
105
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Domain } from "@teslemetry/tesla-protocol/command/universal_message";
|
|
2
|
+
import { AESGCMPersonalizedSignatureData, HMACPersonalizedSignatureData, SessionInfo } from "@teslemetry/tesla-protocol/command/signatures";
|
|
3
|
+
/** Key material derived (but not yet trusted/committed) for a candidate vehicle public key. */
|
|
4
|
+
export interface SessionKeys {
|
|
5
|
+
sharedKey: Buffer;
|
|
6
|
+
hmacKey: Buffer;
|
|
7
|
+
sessionInfoKey: Buffer;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Per-domain (VCSEC / Infotainment) signed-command session: tracks the
|
|
11
|
+
* anti-replay counter/epoch handed out by the vehicle's handshake reply and
|
|
12
|
+
* the HMAC keys derived from it.
|
|
13
|
+
*
|
|
14
|
+
* The vehicle's key is trusted once, at first contact, then pinned for the life
|
|
15
|
+
* of the session, as in Tesla's Go SDK. ECDH is symmetric, so anyone who saw the
|
|
16
|
+
* client's public key can tag a `SessionInfo` naming a key of their own: a tag
|
|
17
|
+
* proves nothing about a key derived from the same reply. `trustedKeys` is the
|
|
18
|
+
* only way a reply reaches key material, so once pinned it is verified with the
|
|
19
|
+
* stored keys, and nothing clears the pin. A re-keyed vehicle needs a new instance.
|
|
20
|
+
*
|
|
21
|
+
* First contact itself is trust on first use. Deriving keys (`keysFor`) is still
|
|
22
|
+
* split from committing them (`commit`), so a reply that fails its tag or carries
|
|
23
|
+
* a whitelist rejection pins nothing. See `Commands.validate_msg`.
|
|
24
|
+
*
|
|
25
|
+
* A `Session` is not itself thread-safe; `withLock` serializes the
|
|
26
|
+
* message-build step (which mutates `counter`) so concurrent commands on the
|
|
27
|
+
* same domain cannot race on the same counter value.
|
|
28
|
+
*
|
|
29
|
+
* The derived keys are native private fields, read through accessors, so a
|
|
30
|
+
* `JSON.stringify` or structured log of a session cannot reach them.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Session {
|
|
33
|
+
#private;
|
|
34
|
+
private readonly deriveSharedKey;
|
|
35
|
+
readonly domain: Domain;
|
|
36
|
+
counter: number;
|
|
37
|
+
epoch?: Buffer;
|
|
38
|
+
delta?: number;
|
|
39
|
+
/** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
|
|
40
|
+
private lastAuthenticatedClockTime?;
|
|
41
|
+
/** `(requestHash, counter)` pairs seen in this epoch, oldest first. */
|
|
42
|
+
private responseCounters;
|
|
43
|
+
private queue;
|
|
44
|
+
constructor(domain: Domain, deriveSharedKey: (vehiclePublicKey: Buffer) => Buffer);
|
|
45
|
+
/** The vehicle key pinned at first contact. There is deliberately no setter. */
|
|
46
|
+
get publicKey(): Buffer | undefined;
|
|
47
|
+
get sharedKey(): Buffer | undefined;
|
|
48
|
+
get hmacKey(): Buffer | undefined;
|
|
49
|
+
get sessionInfoKey(): Buffer | undefined;
|
|
50
|
+
/** Whether the handshake has completed and commands can be signed. */
|
|
51
|
+
get ready(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Derive the shared key and its two HMAC sub-keys for a candidate vehicle
|
|
54
|
+
* public key, without touching any session state. Pure and side-effect
|
|
55
|
+
* free so a caller can verify authenticity first and discard the result
|
|
56
|
+
* if verification fails. Verifying a reply goes through `trustedKeys`.
|
|
57
|
+
*/
|
|
58
|
+
keysFor(vehiclePublicKey: Buffer): SessionKeys;
|
|
59
|
+
/**
|
|
60
|
+
* The keys to verify a reply naming `vehiclePublicKey` with: derived from it
|
|
61
|
+
* at first contact, and the stored keys from then on, never derived again.
|
|
62
|
+
* @throws SessionInfoAuthenticationFault when the session is pinned to another key.
|
|
63
|
+
*/
|
|
64
|
+
trustedKeys(vehiclePublicKey: Buffer): SessionKeys;
|
|
65
|
+
/**
|
|
66
|
+
* Commit an already-authenticated `SessionInfo` plus its `trustedKeys`
|
|
67
|
+
* result. Keys are adopted at first contact only: after that, `keys` is
|
|
68
|
+
* ignored and a different public key is refused. Per the vehicle-command
|
|
69
|
+
* protocol, the anti-replay counter must never roll back within the same
|
|
70
|
+
* epoch, and a clock time that regresses within the same epoch marks the
|
|
71
|
+
* reply as stale/replayed - returns `false` (without mutating any state) in
|
|
72
|
+
* that case so the caller can reject it.
|
|
73
|
+
* @throws SessionInfoAuthenticationFault, changing nothing, for a different public key, or a clock or counter that could not sign a single command.
|
|
74
|
+
*/
|
|
75
|
+
commit(sessionInfo: SessionInfo, keys: SessionKeys): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Record an authenticated response's counter for the request it answers.
|
|
78
|
+
* @returns `false` if that pair was already seen, marking the response a replay.
|
|
79
|
+
*/
|
|
80
|
+
record_response_counter(requestHash: Uint8Array, counter: number): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Advance the anti-replay counter for a command valid for `lifetimeSeconds`,
|
|
83
|
+
* corrected for clock drift against the vehicle (`delta`). The clock keeps
|
|
84
|
+
* running after `commit`, so its limit is checked here too; a session past it
|
|
85
|
+
* recovers when the vehicle starts a new epoch.
|
|
86
|
+
* @throws SessionInfoAuthenticationFault, leaving the counter alone, when either value would overflow its wire field.
|
|
87
|
+
*/
|
|
88
|
+
private nextSignature;
|
|
89
|
+
/**
|
|
90
|
+
* Fresh signature metadata for an HMAC-personalized command, which expires
|
|
91
|
+
* 10 seconds from now.
|
|
92
|
+
*/
|
|
93
|
+
hmacPersonalized(): HMACPersonalizedSignatureData;
|
|
94
|
+
/**
|
|
95
|
+
* Fresh signature metadata for an AES-GCM-personalized command, with a
|
|
96
|
+
* random nonce, which expires 30 seconds from now.
|
|
97
|
+
*/
|
|
98
|
+
aesGcmPersonalized(): AESGCMPersonalizedSignatureData;
|
|
99
|
+
/**
|
|
100
|
+
* Run `fn` with exclusive access to this session, queuing behind any
|
|
101
|
+
* in-flight caller. Not reentrant: `fn` must not call `withLock` on this
|
|
102
|
+
* session. Infotainment holds it for the message build only, and VCSEC for
|
|
103
|
+
* the whole dispatch (see `Commands.dispatch`).
|
|
104
|
+
*/
|
|
105
|
+
withLock<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
106
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { SessionInfoAuthenticationFault } from "../exceptions.mjs";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { AESGCMPersonalizedSignatureData, HMACPersonalizedSignatureData } from "@teslemetry/tesla-protocol/command/signatures";
|
|
4
|
+
// src/signing/session.ts
|
|
5
|
+
/** Wire limit of the `counter` and `expires_at` fields. */
|
|
6
|
+
const UINT32_MAX = 4294967295;
|
|
7
|
+
/** Seconds a signed command stays valid. BLE can be slow, so AES-GCM allows longer. */
|
|
8
|
+
const HMAC_LIFETIME_S = 10;
|
|
9
|
+
const AES_GCM_LIFETIME_S = 30;
|
|
10
|
+
/** Response counters remembered per session, which bounds how far back a replay is caught. */
|
|
11
|
+
const RESPONSE_COUNTER_CACHE_SIZE = 256;
|
|
12
|
+
/**
|
|
13
|
+
* Per-domain (VCSEC / Infotainment) signed-command session: tracks the
|
|
14
|
+
* anti-replay counter/epoch handed out by the vehicle's handshake reply and
|
|
15
|
+
* the HMAC keys derived from it.
|
|
16
|
+
*
|
|
17
|
+
* The vehicle's key is trusted once, at first contact, then pinned for the life
|
|
18
|
+
* of the session, as in Tesla's Go SDK. ECDH is symmetric, so anyone who saw the
|
|
19
|
+
* client's public key can tag a `SessionInfo` naming a key of their own: a tag
|
|
20
|
+
* proves nothing about a key derived from the same reply. `trustedKeys` is the
|
|
21
|
+
* only way a reply reaches key material, so once pinned it is verified with the
|
|
22
|
+
* stored keys, and nothing clears the pin. A re-keyed vehicle needs a new instance.
|
|
23
|
+
*
|
|
24
|
+
* First contact itself is trust on first use. Deriving keys (`keysFor`) is still
|
|
25
|
+
* split from committing them (`commit`), so a reply that fails its tag or carries
|
|
26
|
+
* a whitelist rejection pins nothing. See `Commands.validate_msg`.
|
|
27
|
+
*
|
|
28
|
+
* A `Session` is not itself thread-safe; `withLock` serializes the
|
|
29
|
+
* message-build step (which mutates `counter`) so concurrent commands on the
|
|
30
|
+
* same domain cannot race on the same counter value.
|
|
31
|
+
*
|
|
32
|
+
* The derived keys are native private fields, read through accessors, so a
|
|
33
|
+
* `JSON.stringify` or structured log of a session cannot reach them.
|
|
34
|
+
*/
|
|
35
|
+
var Session = class {
|
|
36
|
+
deriveSharedKey;
|
|
37
|
+
domain;
|
|
38
|
+
counter = 0;
|
|
39
|
+
epoch;
|
|
40
|
+
delta;
|
|
41
|
+
#publicKey;
|
|
42
|
+
#keys;
|
|
43
|
+
/** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
|
|
44
|
+
lastAuthenticatedClockTime;
|
|
45
|
+
/** `(requestHash, counter)` pairs seen in this epoch, oldest first. */
|
|
46
|
+
responseCounters = /* @__PURE__ */ new Set();
|
|
47
|
+
queue = Promise.resolve();
|
|
48
|
+
constructor(domain, deriveSharedKey) {
|
|
49
|
+
this.deriveSharedKey = deriveSharedKey;
|
|
50
|
+
this.domain = domain;
|
|
51
|
+
}
|
|
52
|
+
/** The vehicle key pinned at first contact. There is deliberately no setter. */
|
|
53
|
+
get publicKey() {
|
|
54
|
+
return this.#publicKey;
|
|
55
|
+
}
|
|
56
|
+
get sharedKey() {
|
|
57
|
+
return this.#keys?.sharedKey;
|
|
58
|
+
}
|
|
59
|
+
get hmacKey() {
|
|
60
|
+
return this.#keys?.hmacKey;
|
|
61
|
+
}
|
|
62
|
+
get sessionInfoKey() {
|
|
63
|
+
return this.#keys?.sessionInfoKey;
|
|
64
|
+
}
|
|
65
|
+
/** Whether the handshake has completed and commands can be signed. */
|
|
66
|
+
get ready() {
|
|
67
|
+
return this.epoch !== void 0 && this.#keys !== void 0 && this.delta !== void 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Derive the shared key and its two HMAC sub-keys for a candidate vehicle
|
|
71
|
+
* public key, without touching any session state. Pure and side-effect
|
|
72
|
+
* free so a caller can verify authenticity first and discard the result
|
|
73
|
+
* if verification fails. Verifying a reply goes through `trustedKeys`.
|
|
74
|
+
*/
|
|
75
|
+
keysFor(vehiclePublicKey) {
|
|
76
|
+
const sharedKey = this.deriveSharedKey(vehiclePublicKey);
|
|
77
|
+
return {
|
|
78
|
+
sharedKey,
|
|
79
|
+
hmacKey: crypto.createHmac("sha256", sharedKey).update("authenticated command").digest(),
|
|
80
|
+
sessionInfoKey: crypto.createHmac("sha256", sharedKey).update("session info").digest()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The keys to verify a reply naming `vehiclePublicKey` with: derived from it
|
|
85
|
+
* at first contact, and the stored keys from then on, never derived again.
|
|
86
|
+
* @throws SessionInfoAuthenticationFault when the session is pinned to another key.
|
|
87
|
+
*/
|
|
88
|
+
trustedKeys(vehiclePublicKey) {
|
|
89
|
+
if (this.#publicKey === void 0 || this.#keys === void 0) return this.keysFor(vehiclePublicKey);
|
|
90
|
+
if (!this.#publicKey.equals(vehiclePublicKey)) throw new SessionInfoAuthenticationFault("Session info reply names a vehicle public key that has changed since first contact; discarded. If the vehicle was really re-keyed, build a new instance to trust it.");
|
|
91
|
+
return this.#keys;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Commit an already-authenticated `SessionInfo` plus its `trustedKeys`
|
|
95
|
+
* result. Keys are adopted at first contact only: after that, `keys` is
|
|
96
|
+
* ignored and a different public key is refused. Per the vehicle-command
|
|
97
|
+
* protocol, the anti-replay counter must never roll back within the same
|
|
98
|
+
* epoch, and a clock time that regresses within the same epoch marks the
|
|
99
|
+
* reply as stale/replayed - returns `false` (without mutating any state) in
|
|
100
|
+
* that case so the caller can reject it.
|
|
101
|
+
* @throws SessionInfoAuthenticationFault, changing nothing, for a different public key, or a clock or counter that could not sign a single command.
|
|
102
|
+
*/
|
|
103
|
+
commit(sessionInfo, keys) {
|
|
104
|
+
const trusted = this.#keys === void 0 ? keys : this.trustedKeys(Buffer.from(sessionInfo.publicKey));
|
|
105
|
+
const incomingEpoch = Buffer.from(sessionInfo.epoch);
|
|
106
|
+
const sameEpoch = this.epoch !== void 0 && this.epoch.equals(incomingEpoch);
|
|
107
|
+
if (sameEpoch && this.lastAuthenticatedClockTime !== void 0 && sessionInfo.clockTime < this.lastAuthenticatedClockTime) return false;
|
|
108
|
+
if (sessionInfo.clockTime + AES_GCM_LIFETIME_S > UINT32_MAX || sessionInfo.counter >= UINT32_MAX) throw new SessionInfoAuthenticationFault("Session info reply has a clock or counter no command could be signed with; discarded.");
|
|
109
|
+
this.counter = sameEpoch ? Math.max(this.counter, sessionInfo.counter) : sessionInfo.counter;
|
|
110
|
+
this.epoch = incomingEpoch;
|
|
111
|
+
this.delta = Math.floor(Date.now() / 1e3) - sessionInfo.clockTime;
|
|
112
|
+
this.lastAuthenticatedClockTime = sessionInfo.clockTime;
|
|
113
|
+
this.#publicKey = Buffer.from(sessionInfo.publicKey);
|
|
114
|
+
this.#keys = trusted;
|
|
115
|
+
if (!sameEpoch) this.responseCounters.clear();
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Record an authenticated response's counter for the request it answers.
|
|
120
|
+
* @returns `false` if that pair was already seen, marking the response a replay.
|
|
121
|
+
*/
|
|
122
|
+
record_response_counter(requestHash, counter) {
|
|
123
|
+
const identity = `${Buffer.from(requestHash).toString("hex")}:${counter}`;
|
|
124
|
+
if (this.responseCounters.has(identity)) return false;
|
|
125
|
+
this.responseCounters.add(identity);
|
|
126
|
+
if (this.responseCounters.size > RESPONSE_COUNTER_CACHE_SIZE) this.responseCounters.delete(this.responseCounters.values().next().value);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Advance the anti-replay counter for a command valid for `lifetimeSeconds`,
|
|
131
|
+
* corrected for clock drift against the vehicle (`delta`). The clock keeps
|
|
132
|
+
* running after `commit`, so its limit is checked here too; a session past it
|
|
133
|
+
* recovers when the vehicle starts a new epoch.
|
|
134
|
+
* @throws SessionInfoAuthenticationFault, leaving the counter alone, when either value would overflow its wire field.
|
|
135
|
+
*/
|
|
136
|
+
nextSignature(lifetimeSeconds) {
|
|
137
|
+
if (!this.ready || !this.epoch || this.delta === void 0) throw new Error("Session is not ready: perform the handshake first.");
|
|
138
|
+
const expiresAt = Math.floor(Date.now() / 1e3) - this.delta + lifetimeSeconds;
|
|
139
|
+
if (expiresAt > UINT32_MAX || this.counter >= UINT32_MAX) throw new SessionInfoAuthenticationFault("Session has reached the limit of the vehicle clock or counter, so nothing was signed.");
|
|
140
|
+
this.counter++;
|
|
141
|
+
return {
|
|
142
|
+
epoch: this.epoch,
|
|
143
|
+
counter: this.counter,
|
|
144
|
+
expiresAt
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Fresh signature metadata for an HMAC-personalized command, which expires
|
|
149
|
+
* 10 seconds from now.
|
|
150
|
+
*/
|
|
151
|
+
hmacPersonalized() {
|
|
152
|
+
return HMACPersonalizedSignatureData.create(this.nextSignature(HMAC_LIFETIME_S));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fresh signature metadata for an AES-GCM-personalized command, with a
|
|
156
|
+
* random nonce, which expires 30 seconds from now.
|
|
157
|
+
*/
|
|
158
|
+
aesGcmPersonalized() {
|
|
159
|
+
return AESGCMPersonalizedSignatureData.create({
|
|
160
|
+
...this.nextSignature(AES_GCM_LIFETIME_S),
|
|
161
|
+
nonce: crypto.randomBytes(12)
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Run `fn` with exclusive access to this session, queuing behind any
|
|
166
|
+
* in-flight caller. Not reentrant: `fn` must not call `withLock` on this
|
|
167
|
+
* session. Infotainment holds it for the message build only, and VCSEC for
|
|
168
|
+
* the whole dispatch (see `Commands.dispatch`).
|
|
169
|
+
*/
|
|
170
|
+
async withLock(fn) {
|
|
171
|
+
const previous = this.queue;
|
|
172
|
+
let release;
|
|
173
|
+
this.queue = new Promise((resolve) => {
|
|
174
|
+
release = resolve;
|
|
175
|
+
});
|
|
176
|
+
await previous.catch(() => void 0);
|
|
177
|
+
try {
|
|
178
|
+
return await fn();
|
|
179
|
+
} finally {
|
|
180
|
+
release();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
export { Session };
|