tesla-fleet-api 0.4.0 → 0.6.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.
Files changed (215) hide show
  1. package/README.md +269 -1
  2. package/dist/_virtual/_rolldown/runtime.cjs +10 -0
  3. package/dist/bluetooth/adapters/shared.cjs +38 -0
  4. package/dist/bluetooth/adapters/shared.mjs +36 -0
  5. package/dist/bluetooth/async.cjs +121 -0
  6. package/dist/bluetooth/async.d.cts +10 -0
  7. package/dist/bluetooth/async.d.mts +10 -0
  8. package/dist/bluetooth/async.mjs +116 -0
  9. package/dist/bluetooth/broadcast.cjs +153 -0
  10. package/dist/bluetooth/broadcast.d.cts +59 -0
  11. package/dist/bluetooth/broadcast.d.mts +59 -0
  12. package/dist/bluetooth/broadcast.mjs +151 -0
  13. package/dist/bluetooth/constants.cjs +96 -0
  14. package/dist/bluetooth/constants.d.cts +68 -0
  15. package/dist/bluetooth/constants.d.mts +68 -0
  16. package/dist/bluetooth/constants.mjs +70 -0
  17. package/dist/bluetooth/framing.cjs +125 -0
  18. package/dist/bluetooth/framing.d.cts +45 -0
  19. package/dist/bluetooth/framing.d.mts +45 -0
  20. package/dist/bluetooth/framing.mjs +122 -0
  21. package/dist/bluetooth/homey.cjs +165 -0
  22. package/dist/bluetooth/homey.d.cts +84 -0
  23. package/dist/bluetooth/homey.d.mts +84 -0
  24. package/dist/bluetooth/homey.mjs +159 -0
  25. package/dist/bluetooth/names.cjs +24 -0
  26. package/dist/bluetooth/names.d.cts +10 -0
  27. package/dist/bluetooth/names.d.mts +10 -0
  28. package/dist/bluetooth/names.mjs +20 -0
  29. package/dist/bluetooth/noble.cjs +178 -0
  30. package/dist/bluetooth/noble.d.cts +58 -0
  31. package/dist/bluetooth/noble.d.mts +58 -0
  32. package/dist/bluetooth/noble.mjs +177 -0
  33. package/dist/bluetooth/streamglue.cjs +157 -0
  34. package/dist/bluetooth/streamglue.d.cts +37 -0
  35. package/dist/bluetooth/streamglue.d.mts +37 -0
  36. package/dist/bluetooth/streamglue.mjs +154 -0
  37. package/dist/bluetooth/teslabluetooth.cjs +67 -0
  38. package/dist/bluetooth/teslabluetooth.d.cts +41 -0
  39. package/dist/bluetooth/teslabluetooth.d.mts +41 -0
  40. package/dist/bluetooth/teslabluetooth.mjs +66 -0
  41. package/dist/bluetooth/transport.cjs +10 -0
  42. package/dist/bluetooth/transport.d.cts +97 -0
  43. package/dist/bluetooth/transport.d.mts +97 -0
  44. package/dist/bluetooth/transport.mjs +10 -0
  45. package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
  46. package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
  47. package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
  48. package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
  49. package/dist/bluetooth/verify.cjs +93 -0
  50. package/dist/bluetooth/verify.d.cts +35 -0
  51. package/dist/bluetooth/verify.d.mts +35 -0
  52. package/dist/bluetooth/verify.mjs +91 -0
  53. package/dist/bluetooth.cjs +52 -0
  54. package/dist/bluetooth.d.cts +10 -0
  55. package/dist/bluetooth.d.mts +10 -0
  56. package/dist/bluetooth.mjs +9 -0
  57. package/dist/commands.cjs +1080 -85
  58. package/dist/commands.d.cts +585 -54
  59. package/dist/commands.d.mts +583 -51
  60. package/dist/commands.mjs +1084 -93
  61. package/dist/const.cjs +22 -0
  62. package/dist/const.d.cts +21 -0
  63. package/dist/const.d.mts +21 -0
  64. package/dist/const.mjs +20 -1
  65. package/dist/energy.cjs +24 -19
  66. package/dist/energy.d.cts +21 -15
  67. package/dist/energy.d.mts +21 -15
  68. package/dist/energy.mjs +24 -19
  69. package/dist/energyisland.cjs +38 -0
  70. package/dist/energyisland.mjs +37 -0
  71. package/dist/energyparsers.cjs +118 -0
  72. package/dist/energyparsers.d.cts +39 -0
  73. package/dist/energyparsers.d.mts +39 -0
  74. package/dist/energyparsers.mjs +116 -0
  75. package/dist/energyspecific.cjs +375 -17
  76. package/dist/energyspecific.d.cts +199 -19
  77. package/dist/energyspecific.d.mts +197 -16
  78. package/dist/energyspecific.mjs +370 -18
  79. package/dist/exceptions.cjs +3 -2
  80. package/dist/exceptions.d.cts +3 -2
  81. package/dist/exceptions.d.mts +3 -2
  82. package/dist/exceptions.mjs +3 -2
  83. package/dist/funnel.cjs +297 -0
  84. package/dist/funnel.d.cts +118 -0
  85. package/dist/funnel.d.mts +118 -0
  86. package/dist/funnel.mjs +292 -0
  87. package/dist/index.cjs +61 -0
  88. package/dist/index.d.cts +22 -8
  89. package/dist/index.d.mts +22 -8
  90. package/dist/index.mjs +19 -6
  91. package/dist/log.cjs +75 -0
  92. package/dist/log.d.cts +4 -0
  93. package/dist/log.d.mts +4 -0
  94. package/dist/log.mjs +71 -0
  95. package/dist/oauth.cjs +143 -0
  96. package/dist/oauth.d.cts +69 -0
  97. package/dist/oauth.d.mts +69 -0
  98. package/dist/oauth.mjs +143 -0
  99. package/dist/powerwall/client.cjs +818 -0
  100. package/dist/powerwall/client.d.cts +297 -0
  101. package/dist/powerwall/client.d.mts +297 -0
  102. package/dist/powerwall/client.mjs +807 -0
  103. package/dist/powerwall/components.cjs +188 -0
  104. package/dist/powerwall/components.d.cts +45 -0
  105. package/dist/powerwall/components.d.mts +45 -0
  106. package/dist/powerwall/components.mjs +182 -0
  107. package/dist/powerwall/energysite.cjs +216 -0
  108. package/dist/powerwall/energysite.d.cts +104 -0
  109. package/dist/powerwall/energysite.d.mts +104 -0
  110. package/dist/powerwall/energysite.mjs +214 -0
  111. package/dist/powerwall/exceptions.cjs +59 -0
  112. package/dist/powerwall/exceptions.d.cts +38 -0
  113. package/dist/powerwall/exceptions.d.mts +38 -0
  114. package/dist/powerwall/exceptions.mjs +51 -0
  115. package/dist/powerwall/gateway.cjs +102 -0
  116. package/dist/powerwall/gateway.d.cts +57 -0
  117. package/dist/powerwall/gateway.d.mts +57 -0
  118. package/dist/powerwall/gateway.mjs +101 -0
  119. package/dist/powerwall/merge.cjs +163 -0
  120. package/dist/powerwall/merge.d.cts +63 -0
  121. package/dist/powerwall/merge.d.mts +63 -0
  122. package/dist/powerwall/merge.mjs +159 -0
  123. package/dist/powerwall/models.d.cts +95 -0
  124. package/dist/powerwall/models.d.mts +95 -0
  125. package/dist/powerwall/queries.cjs +17 -0
  126. package/dist/powerwall/queries.mjs +10 -0
  127. package/dist/powerwall/transport.cjs +232 -0
  128. package/dist/powerwall/transport.mjs +227 -0
  129. package/dist/powerwall.cjs +47 -0
  130. package/dist/powerwall.d.cts +9 -0
  131. package/dist/powerwall.d.mts +9 -0
  132. package/dist/powerwall.mjs +8 -0
  133. package/dist/router/base.cjs +203 -0
  134. package/dist/router/base.d.cts +112 -0
  135. package/dist/router/base.d.mts +112 -0
  136. package/dist/router/base.mjs +200 -0
  137. package/dist/router/energysite.cjs +65 -0
  138. package/dist/router/energysite.d.cts +47 -0
  139. package/dist/router/energysite.d.mts +47 -0
  140. package/dist/router/energysite.mjs +60 -0
  141. package/dist/router.cjs +13 -0
  142. package/dist/router.d.cts +3 -1
  143. package/dist/router.d.mts +3 -1
  144. package/dist/router.mjs +3 -1
  145. package/dist/signing/crypto.cjs +28 -0
  146. package/dist/signing/crypto.mjs +26 -1
  147. package/dist/signing/energy.cjs +123 -0
  148. package/dist/signing/energy.d.cts +45 -0
  149. package/dist/signing/energy.d.mts +45 -0
  150. package/dist/signing/energy.mjs +120 -0
  151. package/dist/signing/jws.cjs +229 -0
  152. package/dist/signing/jws.d.cts +45 -0
  153. package/dist/signing/jws.d.mts +45 -0
  154. package/dist/signing/jws.mjs +211 -0
  155. package/dist/signing/keys.cjs +191 -0
  156. package/dist/signing/keys.d.cts +21 -0
  157. package/dist/signing/keys.d.mts +21 -0
  158. package/dist/signing/keys.mjs +182 -0
  159. package/dist/signing/session.cjs +104 -30
  160. package/dist/signing/session.d.cts +60 -23
  161. package/dist/signing/session.d.mts +60 -23
  162. package/dist/signing/session.mjs +105 -31
  163. package/dist/tariff.cjs +27 -0
  164. package/dist/tariff.d.cts +11 -0
  165. package/dist/tariff.d.mts +11 -0
  166. package/dist/tariff.mjs +27 -1
  167. package/dist/teslafleetapi.cjs +256 -55
  168. package/dist/teslafleetapi.d.cts +110 -14
  169. package/dist/teslafleetapi.d.mts +110 -14
  170. package/dist/teslafleetapi.mjs +258 -57
  171. package/dist/teslemetry.cjs +152 -5
  172. package/dist/teslemetry.d.cts +95 -5
  173. package/dist/teslemetry.d.mts +93 -3
  174. package/dist/teslemetry.mjs +151 -6
  175. package/dist/teslemetryenergy.cjs +287 -0
  176. package/dist/teslemetryenergy.d.cts +154 -0
  177. package/dist/teslemetryenergy.d.mts +154 -0
  178. package/dist/teslemetryenergy.mjs +282 -0
  179. package/dist/teslemetryvehicle.cjs +407 -0
  180. package/dist/teslemetryvehicle.d.cts +238 -0
  181. package/dist/teslemetryvehicle.d.mts +238 -0
  182. package/dist/teslemetryvehicle.mjs +405 -0
  183. package/dist/tessie.cjs +42 -5
  184. package/dist/tessie.d.cts +51 -5
  185. package/dist/tessie.d.mts +49 -3
  186. package/dist/tessie.mjs +42 -5
  187. package/dist/tessievehicle.cjs +514 -0
  188. package/dist/tessievehicle.d.cts +333 -0
  189. package/dist/tessievehicle.d.mts +333 -0
  190. package/dist/tessievehicle.mjs +513 -0
  191. package/dist/types/live_status.d.cts +16 -16
  192. package/dist/types/live_status.d.mts +16 -16
  193. package/dist/types/responses.d.cts +5 -1
  194. package/dist/types/responses.d.mts +5 -1
  195. package/dist/types/vehicle.d.cts +7 -1
  196. package/dist/types/vehicle.d.mts +7 -1
  197. package/dist/types.d.cts +3 -3
  198. package/dist/types.d.mts +3 -3
  199. package/dist/util.cjs +41 -0
  200. package/dist/util.d.cts +18 -0
  201. package/dist/util.d.mts +18 -0
  202. package/dist/util.mjs +40 -0
  203. package/dist/vehicle.cjs +79 -36
  204. package/dist/vehicle.d.cts +42 -26
  205. package/dist/vehicle.d.mts +42 -26
  206. package/dist/vehicle.mjs +80 -37
  207. package/dist/vehiclesigned.cjs +20 -5
  208. package/dist/vehiclesigned.d.cts +14 -5
  209. package/dist/vehiclesigned.d.mts +14 -5
  210. package/dist/vehiclesigned.mjs +20 -5
  211. package/dist/vehiclespecific.cjs +126 -15
  212. package/dist/vehiclespecific.d.cts +82 -18
  213. package/dist/vehiclespecific.d.mts +82 -18
  214. package/dist/vehiclespecific.mjs +126 -15
  215. package/package.json +56 -3
@@ -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 };
@@ -1,22 +1,38 @@
1
1
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_exceptions = require("../exceptions.cjs");
2
3
  let crypto = require("crypto");
3
4
  crypto = require_runtime.__toESM(crypto, 1);
4
5
  let _teslemetry_tesla_protocol_command_signatures = require("@teslemetry/tesla-protocol/command/signatures");
5
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;
6
14
  /**
7
15
  * Per-domain (VCSEC / Infotainment) signed-command session: tracks the
8
16
  * anti-replay counter/epoch handed out by the vehicle's handshake reply and
9
17
  * the HMAC keys derived from it.
10
18
  *
11
- * Deriving key material (`keysFor`) is split from committing it (`commit`)
12
- * because the caller must authenticate a `SessionInfo` reply - by verifying
13
- * its `session_info_tag` against the very keys derived here - *before*
14
- * trusting anything in it, including the vehicle public key the shared key
15
- * itself is derived from. See `Commands.validateAndUpdateSession`.
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`.
16
29
  *
17
30
  * A `Session` is not itself thread-safe; `withLock` serializes the
18
31
  * message-build step (which mutates `counter`) so concurrent commands on the
19
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.
20
36
  */
21
37
  var Session = class {
22
38
  deriveSharedKey;
@@ -24,26 +40,39 @@ var Session = class {
24
40
  counter = 0;
25
41
  epoch;
26
42
  delta;
27
- sharedKey;
28
- hmacKey;
29
- sessionInfoKey;
30
- publicKey;
43
+ #publicKey;
44
+ #keys;
31
45
  /** Clock time of the last *authenticated* SessionInfo committed for the current epoch, guarding against replay/rollback. */
32
46
  lastAuthenticatedClockTime;
47
+ /** `(requestHash, counter)` pairs seen in this epoch, oldest first. */
48
+ responseCounters = /* @__PURE__ */ new Set();
33
49
  queue = Promise.resolve();
34
50
  constructor(domain, deriveSharedKey) {
35
51
  this.deriveSharedKey = deriveSharedKey;
36
52
  this.domain = domain;
37
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
+ }
38
67
  /** Whether the handshake has completed and commands can be signed. */
39
68
  get ready() {
40
- return this.epoch !== void 0 && this.hmacKey !== void 0 && this.delta !== void 0;
69
+ return this.epoch !== void 0 && this.#keys !== void 0 && this.delta !== void 0;
41
70
  }
42
71
  /**
43
72
  * Derive the shared key and its two HMAC sub-keys for a candidate vehicle
44
73
  * public key, without touching any session state. Pure and side-effect
45
74
  * free so a caller can verify authenticity first and discard the result
46
- * if verification fails.
75
+ * if verification fails. Verifying a reply goes through `trustedKeys`.
47
76
  */
48
77
  keysFor(vehiclePublicKey) {
49
78
  const sharedKey = this.deriveSharedKey(vehiclePublicKey);
@@ -54,46 +83,91 @@ var Session = class {
54
83
  };
55
84
  }
56
85
  /**
57
- * Commit an already-authenticated `SessionInfo` plus the `keysFor` result
58
- * derived from its public key. Per the vehicle-command protocol, the
59
- * anti-replay counter must never roll back within the same epoch, and a
60
- * clock time that regresses within the same epoch marks the reply as
61
- * stale/replayed - returns `false` (without mutating any state) in that
62
- * case so the caller can reject it.
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.
63
104
  */
64
105
  commit(sessionInfo, keys) {
106
+ const trusted = this.#keys === void 0 ? keys : this.trustedKeys(Buffer.from(sessionInfo.publicKey));
65
107
  const incomingEpoch = Buffer.from(sessionInfo.epoch);
66
108
  const sameEpoch = this.epoch !== void 0 && this.epoch.equals(incomingEpoch);
67
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.");
68
111
  this.counter = sameEpoch ? Math.max(this.counter, sessionInfo.counter) : sessionInfo.counter;
69
112
  this.epoch = incomingEpoch;
70
113
  this.delta = Math.floor(Date.now() / 1e3) - sessionInfo.clockTime;
71
114
  this.lastAuthenticatedClockTime = sessionInfo.clockTime;
72
- this.publicKey = Buffer.from(sessionInfo.publicKey);
73
- this.sharedKey = keys.sharedKey;
74
- this.hmacKey = keys.hmacKey;
75
- this.sessionInfoKey = keys.sessionInfoKey;
115
+ this.#publicKey = Buffer.from(sessionInfo.publicKey);
116
+ this.#keys = trusted;
117
+ if (!sameEpoch) this.responseCounters.clear();
76
118
  return true;
77
119
  }
78
120
  /**
79
- * Advance the anti-replay counter and return fresh signature metadata for
80
- * an HMAC-personalized command. The command expires 10 seconds from now,
81
- * corrected for clock drift against the vehicle (`delta`).
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.
82
123
  */
83
- hmacPersonalized() {
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) {
84
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.");
85
142
  this.counter++;
86
- return _teslemetry_tesla_protocol_command_signatures.HMACPersonalizedSignatureData.create({
143
+ return {
87
144
  epoch: this.epoch,
88
145
  counter: this.counter,
89
- expiresAt: Math.floor(Date.now() / 1e3) - this.delta + 10
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)
90
164
  });
91
165
  }
92
166
  /**
93
167
  * Run `fn` with exclusive access to this session, queuing behind any
94
- * in-flight caller. Held only for as long as `fn` takes - callers should
95
- * keep it scoped to the synchronous message-build step, not a full
96
- * network round trip.
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`).
97
171
  */
98
172
  async withLock(fn) {
99
173
  const previous = this.queue;