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,191 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_exceptions = require("../exceptions.cjs");
|
|
3
|
+
const require_crypto = require("./crypto.cjs");
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
5
|
+
node_crypto = require_runtime.__toESM(node_crypto, 1);
|
|
6
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
|
+
let node_timers_promises = require("node:timers/promises");
|
|
8
|
+
// src/signing/keys.ts
|
|
9
|
+
/** Read a key file for this long while another process finishes writing it. */
|
|
10
|
+
const KEY_READ_RETRY_TIMEOUT_MS = 1e3;
|
|
11
|
+
const KEY_READ_RETRY_INTERVAL_MS = 50;
|
|
12
|
+
const KIND_NAMES = {
|
|
13
|
+
ec: "an EC P-256 private key",
|
|
14
|
+
rsa: "an RSA private key"
|
|
15
|
+
};
|
|
16
|
+
/** Whether `error` is the given Node system error, e.g. `ENOENT`. */
|
|
17
|
+
function isSystemError(error, code) {
|
|
18
|
+
return error?.code === code;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* OpenSSL's error for a missing passphrase differs across Node versions
|
|
22
|
+
* (`ERR_MISSING_PASSPHRASE`, `ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED`), so
|
|
23
|
+
* the PEM's own markers decide.
|
|
24
|
+
*/
|
|
25
|
+
function isEncryptedPem(pem) {
|
|
26
|
+
return /-----BEGIN ENCRYPTED PRIVATE KEY-----|Proc-Type:\s*4,ENCRYPTED/.test(pem.toString("latin1"));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Parse a PEM as a private key of `kind`. A raw scalar carries no curve, so an
|
|
30
|
+
* EC key on any curve but P-256 is the wrong type here. `subject` names the
|
|
31
|
+
* source in errors; the key material never reaches one.
|
|
32
|
+
*/
|
|
33
|
+
function parsePrivateKey(pem, kind, subject) {
|
|
34
|
+
let key;
|
|
35
|
+
try {
|
|
36
|
+
key = node_crypto.default.createPrivateKey(pem);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (isEncryptedPem(pem)) throw new require_exceptions.PrivateKeyError("encrypted", `${subject} is encrypted`, { cause: error });
|
|
39
|
+
throw new require_exceptions.PrivateKeyError("malformed", `${subject} is not a valid PEM private key`, { cause: error });
|
|
40
|
+
}
|
|
41
|
+
if (!(key.asymmetricKeyType === kind && (kind === "rsa" || key.asymmetricKeyDetails?.namedCurve === "prime256v1"))) throw new require_exceptions.PrivateKeyError("wrong_type", `${subject} is not ${KIND_NAMES[kind]}`);
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read and parse a key file, waiting out one that is malformed only because
|
|
46
|
+
* another process is still writing it.
|
|
47
|
+
*/
|
|
48
|
+
async function loadKeyFile(path, kind) {
|
|
49
|
+
const deadline = performance.now() + KEY_READ_RETRY_TIMEOUT_MS;
|
|
50
|
+
while (true) {
|
|
51
|
+
let pem;
|
|
52
|
+
try {
|
|
53
|
+
pem = await (0, node_fs_promises.readFile)(path);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new require_exceptions.PrivateKeyError("unreadable", `Could not read private key file at ${path}`, { cause: error });
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return parsePrivateKey(pem, kind, `Private key file at ${path}`);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.reason !== "malformed" || performance.now() >= deadline) throw error;
|
|
61
|
+
await (0, node_timers_promises.setTimeout)(KEY_READ_RETRY_INTERVAL_MS);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load the key at `path`, or create it. The file is born exclusively at mode
|
|
67
|
+
* 0600, so it is never world-readable; `chmod` covers a umask stricter than
|
|
68
|
+
* that mode. A process that loses the create race reads the winner's file.
|
|
69
|
+
*/
|
|
70
|
+
async function loadOrCreateKeyFile(path, kind, generate) {
|
|
71
|
+
try {
|
|
72
|
+
return await loadKeyFile(path, kind);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (!isSystemError(error.cause, "ENOENT")) throw error;
|
|
75
|
+
}
|
|
76
|
+
const pem = await generate();
|
|
77
|
+
let file;
|
|
78
|
+
try {
|
|
79
|
+
file = await (0, node_fs_promises.open)(path, "wx", 384);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (isSystemError(error, "EEXIST")) return loadKeyFile(path, kind);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await file.chmod(384);
|
|
86
|
+
await file.writeFile(pem);
|
|
87
|
+
} finally {
|
|
88
|
+
await file.close();
|
|
89
|
+
}
|
|
90
|
+
return parsePrivateKey(pem, kind, `Private key file at ${path}`);
|
|
91
|
+
}
|
|
92
|
+
/** The raw 32-byte scalar of a P-256 key. JWK pads `d` to the field size, so leading zeros survive. */
|
|
93
|
+
function rawScalar(key) {
|
|
94
|
+
return Buffer.from(key.export({ format: "jwk" }).d, "base64url");
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get or create the P-256 key file at `path`.
|
|
98
|
+
* @returns The raw 32-byte private key `Commands` takes.
|
|
99
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
100
|
+
*/
|
|
101
|
+
async function ecPrivateKeyFile(path) {
|
|
102
|
+
return rawScalar(await loadOrCreateKeyFile(path, "ec", generate_ec_private_key_pem));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get or create the RSA key file at `path`.
|
|
106
|
+
* @param key_size Modulus length in bits for a new key.
|
|
107
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
108
|
+
*/
|
|
109
|
+
function rsaPrivateKeyFile(path, key_size) {
|
|
110
|
+
return loadOrCreateKeyFile(path, "rsa", () => generate_rsa_private_key_pem(key_size));
|
|
111
|
+
}
|
|
112
|
+
/** Generate a P-256 private key, as the SEC1 PEM Python's `get_private_key` writes. */
|
|
113
|
+
function generate_ec_private_key_pem() {
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
node_crypto.default.generateKeyPair("ec", {
|
|
116
|
+
namedCurve: "P-256",
|
|
117
|
+
publicKeyEncoding: {
|
|
118
|
+
type: "spki",
|
|
119
|
+
format: "pem"
|
|
120
|
+
},
|
|
121
|
+
privateKeyEncoding: {
|
|
122
|
+
type: "sec1",
|
|
123
|
+
format: "pem"
|
|
124
|
+
}
|
|
125
|
+
}, (error, _publicKey, privateKey) => error ? reject(error) : resolve(privateKey));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Generate an RSA private key off the event loop, as the PKCS#1 PEM Python's
|
|
130
|
+
* `get_rsa_private_key` writes.
|
|
131
|
+
* @param modulusLength Key size in bits; 4096 is what Powerwall v1r expects.
|
|
132
|
+
*/
|
|
133
|
+
function generate_rsa_private_key_pem(modulusLength = 4096) {
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
node_crypto.default.generateKeyPair("rsa", {
|
|
136
|
+
modulusLength,
|
|
137
|
+
publicExponent: 65537,
|
|
138
|
+
publicKeyEncoding: {
|
|
139
|
+
type: "spki",
|
|
140
|
+
format: "pem"
|
|
141
|
+
},
|
|
142
|
+
privateKeyEncoding: {
|
|
143
|
+
type: "pkcs1",
|
|
144
|
+
format: "pem"
|
|
145
|
+
}
|
|
146
|
+
}, (error, _publicKey, privateKey) => error ? reject(error) : resolve(privateKey));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The raw 32-byte `Buffer` that `Commands` takes, from a P-256 PEM held in
|
|
151
|
+
* memory.
|
|
152
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
153
|
+
*/
|
|
154
|
+
function ec_private_key_from_pem(pem) {
|
|
155
|
+
return rawScalar(parsePrivateKey(pem, "ec", "Private key PEM"));
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The RSA public key in DER PKCS#1 form, the format an energy gateway expects
|
|
159
|
+
* when registering an authorized client, from a private key PEM held in memory.
|
|
160
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
161
|
+
*/
|
|
162
|
+
function rsa_public_der_pkcs1(pem) {
|
|
163
|
+
return rsaPublicDerPkcs1(parsePrivateKey(pem, "rsa", "Private key PEM"));
|
|
164
|
+
}
|
|
165
|
+
function rsaPublicDerPkcs1(key) {
|
|
166
|
+
return node_crypto.default.createPublicKey(key).export({
|
|
167
|
+
type: "pkcs1",
|
|
168
|
+
format: "der"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** The public key of a raw P-256 private key, as a `KeyObject` for serialization. */
|
|
172
|
+
function ecPublicKey(privateKey) {
|
|
173
|
+
const point = require_crypto.publicKeyFor(privateKey);
|
|
174
|
+
return node_crypto.default.createPublicKey({
|
|
175
|
+
key: {
|
|
176
|
+
kty: "EC",
|
|
177
|
+
crv: "P-256",
|
|
178
|
+
x: point.subarray(1, 33).toString("base64url"),
|
|
179
|
+
y: point.subarray(33).toString("base64url")
|
|
180
|
+
},
|
|
181
|
+
format: "jwk"
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
exports.ecPrivateKeyFile = ecPrivateKeyFile;
|
|
185
|
+
exports.ecPublicKey = ecPublicKey;
|
|
186
|
+
exports.ec_private_key_from_pem = ec_private_key_from_pem;
|
|
187
|
+
exports.generate_ec_private_key_pem = generate_ec_private_key_pem;
|
|
188
|
+
exports.generate_rsa_private_key_pem = generate_rsa_private_key_pem;
|
|
189
|
+
exports.rsaPrivateKeyFile = rsaPrivateKeyFile;
|
|
190
|
+
exports.rsaPublicDerPkcs1 = rsaPublicDerPkcs1;
|
|
191
|
+
exports.rsa_public_der_pkcs1 = rsa_public_der_pkcs1;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
/** Generate a P-256 private key, as the SEC1 PEM Python's `get_private_key` writes. */
|
|
3
|
+
export declare function generate_ec_private_key_pem(): Promise<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Generate an RSA private key off the event loop, as the PKCS#1 PEM Python's
|
|
6
|
+
* `get_rsa_private_key` writes.
|
|
7
|
+
* @param modulusLength Key size in bits; 4096 is what Powerwall v1r expects.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generate_rsa_private_key_pem(modulusLength?: number): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* The raw 32-byte `Buffer` that `Commands` takes, from a P-256 PEM held in
|
|
12
|
+
* memory.
|
|
13
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ec_private_key_from_pem(pem: string | Buffer): Buffer;
|
|
16
|
+
/**
|
|
17
|
+
* The RSA public key in DER PKCS#1 form, the format an energy gateway expects
|
|
18
|
+
* when registering an authorized client, from a private key PEM held in memory.
|
|
19
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rsa_public_der_pkcs1(pem: string | Buffer): Buffer;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
/** Generate a P-256 private key, as the SEC1 PEM Python's `get_private_key` writes. */
|
|
3
|
+
export declare function generate_ec_private_key_pem(): Promise<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Generate an RSA private key off the event loop, as the PKCS#1 PEM Python's
|
|
6
|
+
* `get_rsa_private_key` writes.
|
|
7
|
+
* @param modulusLength Key size in bits; 4096 is what Powerwall v1r expects.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generate_rsa_private_key_pem(modulusLength?: number): Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* The raw 32-byte `Buffer` that `Commands` takes, from a P-256 PEM held in
|
|
12
|
+
* memory.
|
|
13
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ec_private_key_from_pem(pem: string | Buffer): Buffer;
|
|
16
|
+
/**
|
|
17
|
+
* The RSA public key in DER PKCS#1 form, the format an energy gateway expects
|
|
18
|
+
* when registering an authorized client, from a private key PEM held in memory.
|
|
19
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
20
|
+
*/
|
|
21
|
+
export declare function rsa_public_der_pkcs1(pem: string | Buffer): Buffer;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { PrivateKeyError } from "../exceptions.mjs";
|
|
2
|
+
import { publicKeyFor } from "./crypto.mjs";
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import { open, readFile } from "node:fs/promises";
|
|
5
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
|
+
// src/signing/keys.ts
|
|
7
|
+
/** Read a key file for this long while another process finishes writing it. */
|
|
8
|
+
const KEY_READ_RETRY_TIMEOUT_MS = 1e3;
|
|
9
|
+
const KEY_READ_RETRY_INTERVAL_MS = 50;
|
|
10
|
+
const KIND_NAMES = {
|
|
11
|
+
ec: "an EC P-256 private key",
|
|
12
|
+
rsa: "an RSA private key"
|
|
13
|
+
};
|
|
14
|
+
/** Whether `error` is the given Node system error, e.g. `ENOENT`. */
|
|
15
|
+
function isSystemError(error, code) {
|
|
16
|
+
return error?.code === code;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* OpenSSL's error for a missing passphrase differs across Node versions
|
|
20
|
+
* (`ERR_MISSING_PASSPHRASE`, `ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED`), so
|
|
21
|
+
* the PEM's own markers decide.
|
|
22
|
+
*/
|
|
23
|
+
function isEncryptedPem(pem) {
|
|
24
|
+
return /-----BEGIN ENCRYPTED PRIVATE KEY-----|Proc-Type:\s*4,ENCRYPTED/.test(pem.toString("latin1"));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse a PEM as a private key of `kind`. A raw scalar carries no curve, so an
|
|
28
|
+
* EC key on any curve but P-256 is the wrong type here. `subject` names the
|
|
29
|
+
* source in errors; the key material never reaches one.
|
|
30
|
+
*/
|
|
31
|
+
function parsePrivateKey(pem, kind, subject) {
|
|
32
|
+
let key;
|
|
33
|
+
try {
|
|
34
|
+
key = crypto.createPrivateKey(pem);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (isEncryptedPem(pem)) throw new PrivateKeyError("encrypted", `${subject} is encrypted`, { cause: error });
|
|
37
|
+
throw new PrivateKeyError("malformed", `${subject} is not a valid PEM private key`, { cause: error });
|
|
38
|
+
}
|
|
39
|
+
if (!(key.asymmetricKeyType === kind && (kind === "rsa" || key.asymmetricKeyDetails?.namedCurve === "prime256v1"))) throw new PrivateKeyError("wrong_type", `${subject} is not ${KIND_NAMES[kind]}`);
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read and parse a key file, waiting out one that is malformed only because
|
|
44
|
+
* another process is still writing it.
|
|
45
|
+
*/
|
|
46
|
+
async function loadKeyFile(path, kind) {
|
|
47
|
+
const deadline = performance.now() + KEY_READ_RETRY_TIMEOUT_MS;
|
|
48
|
+
while (true) {
|
|
49
|
+
let pem;
|
|
50
|
+
try {
|
|
51
|
+
pem = await readFile(path);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new PrivateKeyError("unreadable", `Could not read private key file at ${path}`, { cause: error });
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return parsePrivateKey(pem, kind, `Private key file at ${path}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.reason !== "malformed" || performance.now() >= deadline) throw error;
|
|
59
|
+
await setTimeout(KEY_READ_RETRY_INTERVAL_MS);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Load the key at `path`, or create it. The file is born exclusively at mode
|
|
65
|
+
* 0600, so it is never world-readable; `chmod` covers a umask stricter than
|
|
66
|
+
* that mode. A process that loses the create race reads the winner's file.
|
|
67
|
+
*/
|
|
68
|
+
async function loadOrCreateKeyFile(path, kind, generate) {
|
|
69
|
+
try {
|
|
70
|
+
return await loadKeyFile(path, kind);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!isSystemError(error.cause, "ENOENT")) throw error;
|
|
73
|
+
}
|
|
74
|
+
const pem = await generate();
|
|
75
|
+
let file;
|
|
76
|
+
try {
|
|
77
|
+
file = await open(path, "wx", 384);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (isSystemError(error, "EEXIST")) return loadKeyFile(path, kind);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
await file.chmod(384);
|
|
84
|
+
await file.writeFile(pem);
|
|
85
|
+
} finally {
|
|
86
|
+
await file.close();
|
|
87
|
+
}
|
|
88
|
+
return parsePrivateKey(pem, kind, `Private key file at ${path}`);
|
|
89
|
+
}
|
|
90
|
+
/** The raw 32-byte scalar of a P-256 key. JWK pads `d` to the field size, so leading zeros survive. */
|
|
91
|
+
function rawScalar(key) {
|
|
92
|
+
return Buffer.from(key.export({ format: "jwk" }).d, "base64url");
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get or create the P-256 key file at `path`.
|
|
96
|
+
* @returns The raw 32-byte private key `Commands` takes.
|
|
97
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
98
|
+
*/
|
|
99
|
+
async function ecPrivateKeyFile(path) {
|
|
100
|
+
return rawScalar(await loadOrCreateKeyFile(path, "ec", generate_ec_private_key_pem));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get or create the RSA key file at `path`.
|
|
104
|
+
* @param key_size Modulus length in bits for a new key.
|
|
105
|
+
* @throws PrivateKeyError for an existing file that cannot be used.
|
|
106
|
+
*/
|
|
107
|
+
function rsaPrivateKeyFile(path, key_size) {
|
|
108
|
+
return loadOrCreateKeyFile(path, "rsa", () => generate_rsa_private_key_pem(key_size));
|
|
109
|
+
}
|
|
110
|
+
/** Generate a P-256 private key, as the SEC1 PEM Python's `get_private_key` writes. */
|
|
111
|
+
function generate_ec_private_key_pem() {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
crypto.generateKeyPair("ec", {
|
|
114
|
+
namedCurve: "P-256",
|
|
115
|
+
publicKeyEncoding: {
|
|
116
|
+
type: "spki",
|
|
117
|
+
format: "pem"
|
|
118
|
+
},
|
|
119
|
+
privateKeyEncoding: {
|
|
120
|
+
type: "sec1",
|
|
121
|
+
format: "pem"
|
|
122
|
+
}
|
|
123
|
+
}, (error, _publicKey, privateKey) => error ? reject(error) : resolve(privateKey));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate an RSA private key off the event loop, as the PKCS#1 PEM Python's
|
|
128
|
+
* `get_rsa_private_key` writes.
|
|
129
|
+
* @param modulusLength Key size in bits; 4096 is what Powerwall v1r expects.
|
|
130
|
+
*/
|
|
131
|
+
function generate_rsa_private_key_pem(modulusLength = 4096) {
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
crypto.generateKeyPair("rsa", {
|
|
134
|
+
modulusLength,
|
|
135
|
+
publicExponent: 65537,
|
|
136
|
+
publicKeyEncoding: {
|
|
137
|
+
type: "spki",
|
|
138
|
+
format: "pem"
|
|
139
|
+
},
|
|
140
|
+
privateKeyEncoding: {
|
|
141
|
+
type: "pkcs1",
|
|
142
|
+
format: "pem"
|
|
143
|
+
}
|
|
144
|
+
}, (error, _publicKey, privateKey) => error ? reject(error) : resolve(privateKey));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* The raw 32-byte `Buffer` that `Commands` takes, from a P-256 PEM held in
|
|
149
|
+
* memory.
|
|
150
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
151
|
+
*/
|
|
152
|
+
function ec_private_key_from_pem(pem) {
|
|
153
|
+
return rawScalar(parsePrivateKey(pem, "ec", "Private key PEM"));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The RSA public key in DER PKCS#1 form, the format an energy gateway expects
|
|
157
|
+
* when registering an authorized client, from a private key PEM held in memory.
|
|
158
|
+
* @throws PrivateKeyError when the PEM cannot be used.
|
|
159
|
+
*/
|
|
160
|
+
function rsa_public_der_pkcs1(pem) {
|
|
161
|
+
return rsaPublicDerPkcs1(parsePrivateKey(pem, "rsa", "Private key PEM"));
|
|
162
|
+
}
|
|
163
|
+
function rsaPublicDerPkcs1(key) {
|
|
164
|
+
return crypto.createPublicKey(key).export({
|
|
165
|
+
type: "pkcs1",
|
|
166
|
+
format: "der"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** The public key of a raw P-256 private key, as a `KeyObject` for serialization. */
|
|
170
|
+
function ecPublicKey(privateKey) {
|
|
171
|
+
const point = publicKeyFor(privateKey);
|
|
172
|
+
return crypto.createPublicKey({
|
|
173
|
+
key: {
|
|
174
|
+
kty: "EC",
|
|
175
|
+
crv: "P-256",
|
|
176
|
+
x: point.subarray(1, 33).toString("base64url"),
|
|
177
|
+
y: point.subarray(33).toString("base64url")
|
|
178
|
+
},
|
|
179
|
+
format: "jwk"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export { ecPrivateKeyFile, ecPublicKey, ec_private_key_from_pem, generate_ec_private_key_pem, generate_rsa_private_key_pem, rsaPrivateKeyFile, rsaPublicDerPkcs1, rsa_public_der_pkcs1 };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_exceptions = require("../exceptions.cjs");
|
|
3
|
+
let crypto = require("crypto");
|
|
4
|
+
crypto = require_runtime.__toESM(crypto, 1);
|
|
5
|
+
let _teslemetry_tesla_protocol_command_signatures = require("@teslemetry/tesla-protocol/command/signatures");
|
|
6
|
+
// src/signing/session.ts
|
|
7
|
+
/** Wire limit of the `counter` and `expires_at` fields. */
|
|
8
|
+
const UINT32_MAX = 4294967295;
|
|
9
|
+
/** Seconds a signed command stays valid. BLE can be slow, so AES-GCM allows longer. */
|
|
10
|
+
const HMAC_LIFETIME_S = 10;
|
|
11
|
+
const AES_GCM_LIFETIME_S = 30;
|
|
12
|
+
/** Response counters remembered per session, which bounds how far back a replay is caught. */
|
|
13
|
+
const RESPONSE_COUNTER_CACHE_SIZE = 256;
|
|
14
|
+
/**
|
|
15
|
+
* Per-domain (VCSEC / Infotainment) signed-command session: tracks the
|
|
16
|
+
* anti-replay counter/epoch handed out by the vehicle's handshake reply and
|
|
17
|
+
* the HMAC keys derived from it.
|
|
18
|
+
*
|
|
19
|
+
* The vehicle's key is trusted once, at first contact, then pinned for the life
|
|
20
|
+
* of the session, as in Tesla's Go SDK. ECDH is symmetric, so anyone who saw the
|
|
21
|
+
* client's public key can tag a `SessionInfo` naming a key of their own: a tag
|
|
22
|
+
* proves nothing about a key derived from the same reply. `trustedKeys` is the
|
|
23
|
+
* only way a reply reaches key material, so once pinned it is verified with the
|
|
24
|
+
* stored keys, and nothing clears the pin. A re-keyed vehicle needs a new instance.
|
|
25
|
+
*
|
|
26
|
+
* First contact itself is trust on first use. Deriving keys (`keysFor`) is still
|
|
27
|
+
* split from committing them (`commit`), so a reply that fails its tag or carries
|
|
28
|
+
* a whitelist rejection pins nothing. See `Commands.validate_msg`.
|
|
29
|
+
*
|
|
30
|
+
* A `Session` is not itself thread-safe; `withLock` serializes the
|
|
31
|
+
* message-build step (which mutates `counter`) so concurrent commands on the
|
|
32
|
+
* same domain cannot race on the same counter value.
|
|
33
|
+
*
|
|
34
|
+
* The derived keys are native private fields, read through accessors, so a
|
|
35
|
+
* `JSON.stringify` or structured log of a session cannot reach them.
|
|
36
|
+
*/
|
|
37
|
+
var Session = class {
|
|
38
|
+
deriveSharedKey;
|
|
39
|
+
domain;
|
|
40
|
+
counter = 0;
|
|
41
|
+
epoch;
|
|
42
|
+
delta;
|
|
43
|
+
#publicKey;
|
|
44
|
+
#keys;
|
|
45
|
+
/** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
|
|
46
|
+
lastAuthenticatedClockTime;
|
|
47
|
+
/** `(requestHash, counter)` pairs seen in this epoch, oldest first. */
|
|
48
|
+
responseCounters = /* @__PURE__ */ new Set();
|
|
49
|
+
queue = Promise.resolve();
|
|
50
|
+
constructor(domain, deriveSharedKey) {
|
|
51
|
+
this.deriveSharedKey = deriveSharedKey;
|
|
52
|
+
this.domain = domain;
|
|
53
|
+
}
|
|
54
|
+
/** The vehicle key pinned at first contact. There is deliberately no setter. */
|
|
55
|
+
get publicKey() {
|
|
56
|
+
return this.#publicKey;
|
|
57
|
+
}
|
|
58
|
+
get sharedKey() {
|
|
59
|
+
return this.#keys?.sharedKey;
|
|
60
|
+
}
|
|
61
|
+
get hmacKey() {
|
|
62
|
+
return this.#keys?.hmacKey;
|
|
63
|
+
}
|
|
64
|
+
get sessionInfoKey() {
|
|
65
|
+
return this.#keys?.sessionInfoKey;
|
|
66
|
+
}
|
|
67
|
+
/** Whether the handshake has completed and commands can be signed. */
|
|
68
|
+
get ready() {
|
|
69
|
+
return this.epoch !== void 0 && this.#keys !== void 0 && this.delta !== void 0;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive the shared key and its two HMAC sub-keys for a candidate vehicle
|
|
73
|
+
* public key, without touching any session state. Pure and side-effect
|
|
74
|
+
* free so a caller can verify authenticity first and discard the result
|
|
75
|
+
* if verification fails. Verifying a reply goes through `trustedKeys`.
|
|
76
|
+
*/
|
|
77
|
+
keysFor(vehiclePublicKey) {
|
|
78
|
+
const sharedKey = this.deriveSharedKey(vehiclePublicKey);
|
|
79
|
+
return {
|
|
80
|
+
sharedKey,
|
|
81
|
+
hmacKey: crypto.default.createHmac("sha256", sharedKey).update("authenticated command").digest(),
|
|
82
|
+
sessionInfoKey: crypto.default.createHmac("sha256", sharedKey).update("session info").digest()
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The keys to verify a reply naming `vehiclePublicKey` with: derived from it
|
|
87
|
+
* at first contact, and the stored keys from then on, never derived again.
|
|
88
|
+
* @throws SessionInfoAuthenticationFault when the session is pinned to another key.
|
|
89
|
+
*/
|
|
90
|
+
trustedKeys(vehiclePublicKey) {
|
|
91
|
+
if (this.#publicKey === void 0 || this.#keys === void 0) return this.keysFor(vehiclePublicKey);
|
|
92
|
+
if (!this.#publicKey.equals(vehiclePublicKey)) throw new require_exceptions.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.");
|
|
93
|
+
return this.#keys;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Commit an already-authenticated `SessionInfo` plus its `trustedKeys`
|
|
97
|
+
* result. Keys are adopted at first contact only: after that, `keys` is
|
|
98
|
+
* ignored and a different public key is refused. Per the vehicle-command
|
|
99
|
+
* protocol, the anti-replay counter must never roll back within the same
|
|
100
|
+
* epoch, and a clock time that regresses within the same epoch marks the
|
|
101
|
+
* reply as stale/replayed - returns `false` (without mutating any state) in
|
|
102
|
+
* that case so the caller can reject it.
|
|
103
|
+
* @throws SessionInfoAuthenticationFault, changing nothing, for a different public key, or a clock or counter that could not sign a single command.
|
|
104
|
+
*/
|
|
105
|
+
commit(sessionInfo, keys) {
|
|
106
|
+
const trusted = this.#keys === void 0 ? keys : this.trustedKeys(Buffer.from(sessionInfo.publicKey));
|
|
107
|
+
const incomingEpoch = Buffer.from(sessionInfo.epoch);
|
|
108
|
+
const sameEpoch = this.epoch !== void 0 && this.epoch.equals(incomingEpoch);
|
|
109
|
+
if (sameEpoch && this.lastAuthenticatedClockTime !== void 0 && sessionInfo.clockTime < this.lastAuthenticatedClockTime) return false;
|
|
110
|
+
if (sessionInfo.clockTime + AES_GCM_LIFETIME_S > UINT32_MAX || sessionInfo.counter >= UINT32_MAX) throw new require_exceptions.SessionInfoAuthenticationFault("Session info reply has a clock or counter no command could be signed with; discarded.");
|
|
111
|
+
this.counter = sameEpoch ? Math.max(this.counter, sessionInfo.counter) : sessionInfo.counter;
|
|
112
|
+
this.epoch = incomingEpoch;
|
|
113
|
+
this.delta = Math.floor(Date.now() / 1e3) - sessionInfo.clockTime;
|
|
114
|
+
this.lastAuthenticatedClockTime = sessionInfo.clockTime;
|
|
115
|
+
this.#publicKey = Buffer.from(sessionInfo.publicKey);
|
|
116
|
+
this.#keys = trusted;
|
|
117
|
+
if (!sameEpoch) this.responseCounters.clear();
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Record an authenticated response's counter for the request it answers.
|
|
122
|
+
* @returns `false` if that pair was already seen, marking the response a replay.
|
|
123
|
+
*/
|
|
124
|
+
record_response_counter(requestHash, counter) {
|
|
125
|
+
const identity = `${Buffer.from(requestHash).toString("hex")}:${counter}`;
|
|
126
|
+
if (this.responseCounters.has(identity)) return false;
|
|
127
|
+
this.responseCounters.add(identity);
|
|
128
|
+
if (this.responseCounters.size > RESPONSE_COUNTER_CACHE_SIZE) this.responseCounters.delete(this.responseCounters.values().next().value);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Advance the anti-replay counter for a command valid for `lifetimeSeconds`,
|
|
133
|
+
* corrected for clock drift against the vehicle (`delta`). The clock keeps
|
|
134
|
+
* running after `commit`, so its limit is checked here too; a session past it
|
|
135
|
+
* recovers when the vehicle starts a new epoch.
|
|
136
|
+
* @throws SessionInfoAuthenticationFault, leaving the counter alone, when either value would overflow its wire field.
|
|
137
|
+
*/
|
|
138
|
+
nextSignature(lifetimeSeconds) {
|
|
139
|
+
if (!this.ready || !this.epoch || this.delta === void 0) throw new Error("Session is not ready: perform the handshake first.");
|
|
140
|
+
const expiresAt = Math.floor(Date.now() / 1e3) - this.delta + lifetimeSeconds;
|
|
141
|
+
if (expiresAt > UINT32_MAX || this.counter >= UINT32_MAX) throw new require_exceptions.SessionInfoAuthenticationFault("Session has reached the limit of the vehicle clock or counter, so nothing was signed.");
|
|
142
|
+
this.counter++;
|
|
143
|
+
return {
|
|
144
|
+
epoch: this.epoch,
|
|
145
|
+
counter: this.counter,
|
|
146
|
+
expiresAt
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Fresh signature metadata for an HMAC-personalized command, which expires
|
|
151
|
+
* 10 seconds from now.
|
|
152
|
+
*/
|
|
153
|
+
hmacPersonalized() {
|
|
154
|
+
return _teslemetry_tesla_protocol_command_signatures.HMACPersonalizedSignatureData.create(this.nextSignature(HMAC_LIFETIME_S));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Fresh signature metadata for an AES-GCM-personalized command, with a
|
|
158
|
+
* random nonce, which expires 30 seconds from now.
|
|
159
|
+
*/
|
|
160
|
+
aesGcmPersonalized() {
|
|
161
|
+
return _teslemetry_tesla_protocol_command_signatures.AESGCMPersonalizedSignatureData.create({
|
|
162
|
+
...this.nextSignature(AES_GCM_LIFETIME_S),
|
|
163
|
+
nonce: crypto.default.randomBytes(12)
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Run `fn` with exclusive access to this session, queuing behind any
|
|
168
|
+
* in-flight caller. Not reentrant: `fn` must not call `withLock` on this
|
|
169
|
+
* session. Infotainment holds it for the message build only, and VCSEC for
|
|
170
|
+
* the whole dispatch (see `Commands.dispatch`).
|
|
171
|
+
*/
|
|
172
|
+
async withLock(fn) {
|
|
173
|
+
const previous = this.queue;
|
|
174
|
+
let release;
|
|
175
|
+
this.queue = new Promise((resolve) => {
|
|
176
|
+
release = resolve;
|
|
177
|
+
});
|
|
178
|
+
await previous.catch(() => void 0);
|
|
179
|
+
try {
|
|
180
|
+
return await fn();
|
|
181
|
+
} finally {
|
|
182
|
+
release();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
exports.Session = Session;
|