sliftutils 1.4.88 → 1.5.1

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/index.d.ts CHANGED
@@ -72,17 +72,26 @@ declare module "sliftutils/misc/https/certs" {
72
72
  import * as forge from "node-forge";
73
73
  import { MaybePromise } from "socket-function/src/types";
74
74
  export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
75
- export declare const identityStorageKey = "machineCA_10";
75
+ export declare const identityStorageKey = "machineCA_11";
76
76
  export type IdentityStorageType = {
77
77
  domain: string;
78
78
  certB64: string;
79
79
  keyB64: string;
80
80
  };
81
+ export type BrowserIdentityStorageType = {
82
+ domain: string;
83
+ certB64: string;
84
+ privateKey: CryptoKey;
85
+ };
81
86
  export interface X509KeyPair {
82
87
  domain: string;
83
88
  cert: Buffer;
84
- key: Buffer;
89
+ key: Buffer | CryptoKey;
85
90
  }
91
+ export type NonExtractableKeyPair = {
92
+ publicKeyBytes: Buffer;
93
+ privateKey: CryptoKey;
94
+ };
86
95
  export declare function getCommonName(cert: Buffer | string): string;
87
96
  export declare function createX509(config: {
88
97
  domain: string;
@@ -91,44 +100,46 @@ declare module "sliftutils/misc/https/certs" {
91
100
  keyPair: {
92
101
  publicKey: forge.Ed25519PublicKey;
93
102
  privateKey: forge.Ed25519PrivateKey;
94
- } | forge.pki.KeyPair;
95
- }): X509KeyPair;
103
+ } | forge.pki.KeyPair | NonExtractableKeyPair;
104
+ }): MaybePromise<X509KeyPair>;
105
+ export declare function generateNonExtractableKeyPair(): Promise<NonExtractableKeyPair>;
96
106
  export declare function privateKeyToPem(buffer: forge.pki.PrivateKey | forge.Ed25519PrivateKey): string;
97
107
  export declare function parseCert(PEMorDER: string | Buffer): forge.pki.Certificate;
98
108
  export declare function getPublicIdentifier(PEMorDER: string | Buffer): Buffer;
99
109
  export declare const sign: (keyPair: {
100
- key: string | Buffer;
101
- }, data: unknown) => string;
102
- export declare function verify(cert: string, signature: string, data: unknown): boolean;
110
+ key: string | Buffer | CryptoKey;
111
+ }, data: unknown) => MaybePromise<string>;
112
+ export declare function verify(cert: string, signature: string, data: unknown): void;
103
113
  export declare function validateCACert(domain: string, cert: string | Buffer): void;
104
114
  export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
105
115
  export declare function generateKeyPair(): forge.pki.rsa.KeyPair;
106
116
  export declare function generateRSAKeyPair(): forge.pki.rsa.KeyPair;
107
- export declare function generateTestCA(domain: string): X509KeyPair;
117
+ export declare function generateTestCA(domain: string): MaybePromise<X509KeyPair>;
108
118
  export declare function createCertFromCA(config: {
109
119
  CAKeyPair: X509KeyPair;
110
- }): X509KeyPair;
111
- export declare function getMachineId(domainName: string): string;
120
+ }): MaybePromise<X509KeyPair>;
121
+ export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
112
122
  export type NodeIdParts = {
113
123
  threadId: string;
114
124
  machineId: string;
115
125
  domain: string;
116
126
  port: number;
117
127
  };
118
- export declare function decodeNodeId(nodeId: string): NodeIdParts | undefined;
119
- export declare function decodeNodeIdAssert(nodeId: string): NodeIdParts;
128
+ export declare function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined;
129
+ export declare function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts;
120
130
  export declare function encodeNodeId(parts: NodeIdParts): string;
121
131
  export declare function setIdentityCARaw(domain: string, json: string): Promise<void>;
122
132
  export declare function loadIdentityCA(domain: string): Promise<void>;
123
133
  export declare function getIdentityCA(domain: string): X509KeyPair;
124
134
  export declare function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>;
125
135
  export declare function getOwnMachineId(domain: string): string;
136
+ export declare function getOwnThreadId(domain: string): string;
126
137
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
127
138
  export declare function verifyMachineIdForPublicKey(config: {
128
139
  machineId: string;
129
140
  publicKey: Buffer;
130
141
  }): boolean;
131
- export declare function getThreadKeyCert(domain: string): X509KeyPair;
142
+ export declare function getThreadKeyCert(domain: string): MaybePromise<X509KeyPair>;
132
143
  export declare const createTestBrowserKeyCert: {
133
144
  (): Promise<X509KeyPair>;
134
145
  reset(): void;
@@ -212,11 +223,18 @@ declare module "sliftutils/misc/https/node-forge-ed25519" {
212
223
  static publicKeyToPem(key: Ed25519PublicKey): string;
213
224
  static publicKeyFromPem(pem: string): Ed25519PublicKey;
214
225
  }
226
+ namespace pki {
227
+ function getTBSCertificate(cert: pki.Certificate): asn1.Asn1;
228
+ }
215
229
  }
216
230
  }
217
231
 
218
232
  declare module "sliftutils/misc/https/persistentLocalStorage" {
219
233
  import { MaybePromise } from "socket-function/src/types";
234
+ export declare function getIDBKeyStore<T>(appName: string, key: string): {
235
+ get(): Promise<T | undefined>;
236
+ set(value: T | undefined): Promise<void>;
237
+ };
220
238
  export declare function getKeyStore<T>(appName: string, key: string): {
221
239
  get(): MaybePromise<T | undefined>;
222
240
  set(value: T | null): MaybePromise<void>;
@@ -291,6 +309,11 @@ declare module "sliftutils/misc/random" {
291
309
 
292
310
  }
293
311
 
312
+ declare module "sliftutils/misc/strings" {
313
+ export declare function ellipsize(text: string, maxLength: number): string;
314
+
315
+ }
316
+
294
317
  declare module "sliftutils/misc/types" {
295
318
  export declare function isDefined<T>(value: T | undefined | null): value is T;
296
319
  export declare function freezeObject(value: unknown): unknown;
@@ -5,17 +5,26 @@
5
5
  import * as forge from "node-forge";
6
6
  import { MaybePromise } from "socket-function/src/types";
7
7
  export declare const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
8
- export declare const identityStorageKey = "machineCA_10";
8
+ export declare const identityStorageKey = "machineCA_11";
9
9
  export type IdentityStorageType = {
10
10
  domain: string;
11
11
  certB64: string;
12
12
  keyB64: string;
13
13
  };
14
+ export type BrowserIdentityStorageType = {
15
+ domain: string;
16
+ certB64: string;
17
+ privateKey: CryptoKey;
18
+ };
14
19
  export interface X509KeyPair {
15
20
  domain: string;
16
21
  cert: Buffer;
17
- key: Buffer;
22
+ key: Buffer | CryptoKey;
18
23
  }
24
+ export type NonExtractableKeyPair = {
25
+ publicKeyBytes: Buffer;
26
+ privateKey: CryptoKey;
27
+ };
19
28
  export declare function getCommonName(cert: Buffer | string): string;
20
29
  export declare function createX509(config: {
21
30
  domain: string;
@@ -24,44 +33,46 @@ export declare function createX509(config: {
24
33
  keyPair: {
25
34
  publicKey: forge.Ed25519PublicKey;
26
35
  privateKey: forge.Ed25519PrivateKey;
27
- } | forge.pki.KeyPair;
28
- }): X509KeyPair;
36
+ } | forge.pki.KeyPair | NonExtractableKeyPair;
37
+ }): MaybePromise<X509KeyPair>;
38
+ export declare function generateNonExtractableKeyPair(): Promise<NonExtractableKeyPair>;
29
39
  export declare function privateKeyToPem(buffer: forge.pki.PrivateKey | forge.Ed25519PrivateKey): string;
30
40
  export declare function parseCert(PEMorDER: string | Buffer): forge.pki.Certificate;
31
41
  export declare function getPublicIdentifier(PEMorDER: string | Buffer): Buffer;
32
42
  export declare const sign: (keyPair: {
33
- key: string | Buffer;
34
- }, data: unknown) => string;
35
- export declare function verify(cert: string, signature: string, data: unknown): boolean;
43
+ key: string | Buffer | CryptoKey;
44
+ }, data: unknown) => MaybePromise<string>;
45
+ export declare function verify(cert: string, signature: string, data: unknown): void;
36
46
  export declare function validateCACert(domain: string, cert: string | Buffer): void;
37
47
  export declare function validateCertificate(domain: string, cert: Buffer | string, issuerCert: Buffer | string): void;
38
48
  export declare function generateKeyPair(): forge.pki.rsa.KeyPair;
39
49
  export declare function generateRSAKeyPair(): forge.pki.rsa.KeyPair;
40
- export declare function generateTestCA(domain: string): X509KeyPair;
50
+ export declare function generateTestCA(domain: string): MaybePromise<X509KeyPair>;
41
51
  export declare function createCertFromCA(config: {
42
52
  CAKeyPair: X509KeyPair;
43
- }): X509KeyPair;
44
- export declare function getMachineId(domainName: string): string;
53
+ }): MaybePromise<X509KeyPair>;
54
+ export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
45
55
  export type NodeIdParts = {
46
56
  threadId: string;
47
57
  machineId: string;
48
58
  domain: string;
49
59
  port: number;
50
60
  };
51
- export declare function decodeNodeId(nodeId: string): NodeIdParts | undefined;
52
- export declare function decodeNodeIdAssert(nodeId: string): NodeIdParts;
61
+ export declare function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined;
62
+ export declare function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts;
53
63
  export declare function encodeNodeId(parts: NodeIdParts): string;
54
64
  export declare function setIdentityCARaw(domain: string, json: string): Promise<void>;
55
65
  export declare function loadIdentityCA(domain: string): Promise<void>;
56
66
  export declare function getIdentityCA(domain: string): X509KeyPair;
57
67
  export declare function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>;
58
68
  export declare function getOwnMachineId(domain: string): string;
69
+ export declare function getOwnThreadId(domain: string): string;
59
70
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
60
71
  export declare function verifyMachineIdForPublicKey(config: {
61
72
  machineId: string;
62
73
  publicKey: Buffer;
63
74
  }): boolean;
64
- export declare function getThreadKeyCert(domain: string): X509KeyPair;
75
+ export declare function getThreadKeyCert(domain: string): MaybePromise<X509KeyPair>;
65
76
  export declare const createTestBrowserKeyCert: {
66
77
  (): Promise<X509KeyPair>;
67
78
  reset(): void;
@@ -18,7 +18,8 @@ import { getNodeIdDomain, getNodeIdDomainMaybeUndefined, getNodeIdLocation } fro
18
18
  import { MaybePromise } from "socket-function/src/types";
19
19
  import { SocketFunction } from "socket-function/SocketFunction";
20
20
  import { resetAllNodeCallFactories } from "socket-function/src/nodeCache";
21
- import { getKeyStore } from "./persistentLocalStorage";
21
+ import { getKeyStore, getIDBKeyStore } from "./persistentLocalStorage";
22
+ import { ellipsize } from "../strings";
22
23
 
23
24
  setFlag(require, "node-forge", "allowclient", true);
24
25
  setFlag(require, "js-sha256", "allowclient", true);
@@ -27,14 +28,22 @@ const timeInDay = 1000 * 60 * 60 * 24;
27
28
 
28
29
  export const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
29
30
 
30
- export const identityStorageKey = "machineCA_10";
31
+ export const identityStorageKey = "machineCA_11";
31
32
  export type IdentityStorageType = { domain: string; certB64: string; keyB64: string };
33
+ // In the browser the private key is a non-extractable CryptoKey. IndexedDB structured-clones
34
+ // it, so it persists, but the raw key bytes can never be read by JavaScript (only used
35
+ // via crypto.subtle.sign). This protects against exfiltration of stored keys.
36
+ export type BrowserIdentityStorageType = { domain: string; certB64: string; privateKey: CryptoKey };
32
37
 
33
38
  function getIdentityStore(domain: string) {
34
39
  return getKeyStore<IdentityStorageType>(domain, identityStorageKey);
35
40
  }
41
+ function getBrowserIdentityStore(domain: string) {
42
+ return getIDBKeyStore<BrowserIdentityStorageType>(domain, identityStorageKey);
43
+ }
36
44
 
37
- export interface X509KeyPair { domain: string; cert: Buffer; key: Buffer; }
45
+ export interface X509KeyPair { domain: string; cert: Buffer; key: Buffer | CryptoKey; }
46
+ export type NonExtractableKeyPair = { publicKeyBytes: Buffer; privateKey: CryptoKey };
38
47
 
39
48
  export function getCommonName(cert: Buffer | string) {
40
49
  let subject = new crypto.X509Certificate(cert).subject;
@@ -52,14 +61,18 @@ export function createX509(
52
61
  keyPair: {
53
62
  publicKey: forge.Ed25519PublicKey;
54
63
  privateKey: forge.Ed25519PrivateKey;
55
- } | forge.pki.KeyPair;
64
+ } | forge.pki.KeyPair | NonExtractableKeyPair;
56
65
  }
57
- ): X509KeyPair {
58
- return measureBlock(function createX509() {
66
+ ): MaybePromise<X509KeyPair> {
67
+ return measureBlock(function createX509(): MaybePromise<X509KeyPair> {
59
68
  let { domain, issuer, lifeSpan, keyPair } = config;
60
69
 
61
70
  let certObj = forge.pki.createCertificate();
62
- certObj.publicKey = keyPair.publicKey;
71
+ if ("publicKeyBytes" in keyPair) {
72
+ certObj.publicKey = { publicKeyBytes: keyPair.publicKeyBytes, keyType: forge.pki.oids["EdDSA25519"] } as unknown as forge.pki.PublicKey;
73
+ } else {
74
+ certObj.publicKey = keyPair.publicKey;
75
+ }
63
76
  certObj.serialNumber = "01";
64
77
  // Give it 5 minutes before now. If we give it too much time, it can look like the cert is really
65
78
  // old, which will trigger various processes to try to get a fresher one (as if it lasts for
@@ -99,26 +112,33 @@ export function createX509(
99
112
  //{ type: 7, ip: "127.0.0.1" }
100
113
  ]
101
114
  },
102
- // NOTE: nameConstraints are supported with our branch. But... chrome doesn't support them, so there's no point in using them.
115
+ // NOTE: nameConstraints require our forked node-forge:
103
116
  // "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
104
- // {
105
- // name: "nameConstraints",
106
- // permittedSubtrees: [
107
- // // Chrome doesn't respect nameConstraints per https://bugs.chromium.org/p/chromium/issues/detail?id=1072083,
108
- // // as the spec decided that "free to process or ignore such information" (when present in self
109
- // // signed certificates), and therefore the chrome implementation decided "The first order is to behave predictably",
110
- // // so... they're not going to support it, because why have a feature in one place, if it isn't
111
- // // on android as well... ugh...
112
- // // Works fine on Edge though
113
- // { type: 2, value: forge.util.encodeUtf8(domain) },
114
- // { type: 2, value: forge.util.encodeUtf8(localHostDomain) },
115
- // ]
116
- // },
117
+ // Chrome ignores them (https://bugs.chromium.org/p/chromium/issues/detail?id=1072083),
118
+ // but our own validation (validateCACert/validateCertificate) enforces them.
119
+ {
120
+ name: "nameConstraints",
121
+ permittedSubtrees: [
122
+ { type: 2, value: forge.util.encodeUtf8(domain) },
123
+ { type: 2, value: forge.util.encodeUtf8(localHostDomain) },
124
+ ]
125
+ },
117
126
  ]);
118
127
  certObj.setExtensions(extensions);
119
128
 
120
129
 
121
- measureBlock(function sign() {
130
+ let signResult = measureBlock(function sign(): MaybePromise<void> {
131
+ let signingCryptoKey: CryptoKey | undefined;
132
+ if (issuer === "self") {
133
+ if ("publicKeyBytes" in keyPair) {
134
+ signingCryptoKey = keyPair.privateKey;
135
+ }
136
+ } else if (issuer.key instanceof CryptoKey) {
137
+ signingCryptoKey = issuer.key;
138
+ }
139
+ if (signingCryptoKey) {
140
+ return signCertWithCryptoKey(certObj, signingCryptoKey);
141
+ }
122
142
  if (issuer === "self") {
123
143
  certObj.sign(keyPair.privateKey as any, forge.md.sha256.create());
124
144
  } else {
@@ -126,15 +146,44 @@ export function createX509(
126
146
  }
127
147
  });
128
148
 
129
- return measureBlock(function toPems() {
130
- return {
131
- domain,
132
- cert: Buffer.from(forge.pki.certificateToPem(certObj)),
133
- key: Buffer.from(privateKeyToPem(keyPair.privateKey)),
134
- };
135
- });
149
+ function toPems() {
150
+ return measureBlock(function toPems() {
151
+ let key: Buffer | CryptoKey;
152
+ if ("publicKeyBytes" in keyPair) {
153
+ key = keyPair.privateKey;
154
+ } else {
155
+ key = Buffer.from(privateKeyToPem(keyPair.privateKey));
156
+ }
157
+ return {
158
+ domain,
159
+ cert: Buffer.from(forge.pki.certificateToPem(certObj)),
160
+ key,
161
+ };
162
+ });
163
+ }
164
+ if (signResult instanceof Promise) {
165
+ return signResult.then(toPems);
166
+ }
167
+ return toPems();
136
168
  });
137
169
  }
170
+
171
+ // Replicates the forked forge's ed25519 cert.sign, except the signature is produced by
172
+ // crypto.subtle, so the private key can be a non-extractable CryptoKey.
173
+ async function signCertWithCryptoKey(certObj: forge.pki.Certificate, privateKey: CryptoKey): Promise<void> {
174
+ certObj.signatureOid = certObj.siginfo.algorithmOid = forge.pki.oids["EdDSA25519"];
175
+ let tbs = forge.pki.getTBSCertificate(certObj);
176
+ let tbsDer = forge.asn1.toDer(tbs).getBytes();
177
+ let signature = await globalThis.crypto.subtle.sign("Ed25519", privateKey, Buffer.from(tbsDer, "binary"));
178
+ certObj.tbsCertificate = tbs;
179
+ certObj.signature = Buffer.from(signature).toString("binary");
180
+ }
181
+
182
+ export async function generateNonExtractableKeyPair(): Promise<NonExtractableKeyPair> {
183
+ let keyPair = await globalThis.crypto.subtle.generateKey("Ed25519", false, ["sign", "verify"]) as CryptoKeyPair;
184
+ let publicKeyBytes = Buffer.from(await globalThis.crypto.subtle.exportKey("raw", keyPair.publicKey));
185
+ return { publicKeyBytes, privateKey: keyPair.privateKey };
186
+ }
138
187
  export function privateKeyToPem(buffer: forge.pki.PrivateKey | forge.Ed25519PrivateKey) {
139
188
  if ("privateKeyBytes" in buffer) {
140
189
  return forge.ed25519.privateKeyToPem(buffer);
@@ -176,8 +225,13 @@ function isED25519(key: string | Buffer) {
176
225
  }
177
226
 
178
227
  // EQUIVALENT TO: `crypto.createSign("SHA256").update(JSON.stringify(payload)).sign(keyCert.key, "binary")`
179
- export const sign = measureWrap(function sign(keyPair: { key: string | Buffer }, data: unknown): string {
228
+ export const sign = measureWrap(function sign(keyPair: { key: string | Buffer | CryptoKey }, data: unknown): MaybePromise<string> {
180
229
  let dataStr = JSON.stringify(data);
230
+ if (keyPair.key instanceof CryptoKey) {
231
+ // "binary" encoding matches how forge interprets the string on the verify side
232
+ return globalThis.crypto.subtle.sign("Ed25519", keyPair.key, Buffer.from(dataStr, "binary"))
233
+ .then(signature => Buffer.from(signature).toString("binary"));
234
+ }
181
235
  if (isED25519(keyPair.key)) {
182
236
  let privateKey = (forge.pki.ed25519 as any).privateKeyFromPem(keyPair.key.toString());
183
237
  return privateKey.sign(dataStr);
@@ -191,7 +245,9 @@ export const sign = measureWrap(function sign(keyPair: { key: string | Buffer },
191
245
 
192
246
  export function verify(cert: string, signature: string, data: unknown) {
193
247
  let certObj = parseCert(cert);
194
- return (certObj.publicKey as forge.pki.rsa.PublicKey).verify(JSON.stringify(data), signature);
248
+ if (!(certObj.publicKey as forge.pki.rsa.PublicKey).verify(JSON.stringify(data), signature)) {
249
+ throw new Error(`Signature verification failed. Signature: ${JSON.stringify(signature)} | Data: ${ellipsize(JSON.stringify(data), 1024)}`);
250
+ }
195
251
  }
196
252
 
197
253
  function normalizeCertToPEM(PEMorDER: string | Buffer): string {
@@ -211,7 +267,7 @@ function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } | forg
211
267
  } else {
212
268
  bytes = Buffer.from(new Uint32Array((publicKey as any).n.data).buffer);
213
269
  }
214
- return "b" + sha265.sha256(Buffer.from(bytes)).slice(0, 16).replaceAll("+", "-").replaceAll("/", "_");
270
+ return "b" + Buffer.from(sha265.sha256.array(Buffer.from(bytes))).toString("base64").slice(0, 16).replaceAll("+", "-").replaceAll("/", "_");
215
271
  }
216
272
 
217
273
  export function validateCACert(domain: string, cert: string | Buffer) {
@@ -385,6 +441,31 @@ export function generateTestCA(domain: string) {
385
441
 
386
442
  let identityCA = cache((domain: string) => {
387
443
  let identityCA = lazy((async (): Promise<X509KeyPair> => {
444
+ if (!isNode()) {
445
+ let store = getBrowserIdentityStore(domain);
446
+ let caCached = await store.get();
447
+ if (!caCached) {
448
+ console.log(`Generating new identity CA (non-extractable browser key)`);
449
+ const keyPair = await generateNonExtractableKeyPair();
450
+ let caPublicKeyPart = getDomainPartFromPublicKey(keyPair.publicKeyBytes);
451
+ let fullDomain = `${caPublicKeyPart}.${domain}`;
452
+ let value = await createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
453
+ caCached = {
454
+ domain: value.domain,
455
+ certB64: value.cert.toString("base64"),
456
+ privateKey: keyPair.privateKey,
457
+ };
458
+ await store.set(caCached);
459
+ }
460
+ let result = {
461
+ domain: caCached.domain,
462
+ cert: Buffer.from(caCached.certB64, "base64"),
463
+ key: caCached.privateKey,
464
+ };
465
+ trustCertificate(result.cert.toString());
466
+ identityCA.set(result);
467
+ return result;
468
+ }
388
469
  let identityCACached = getIdentityStore(domain);
389
470
  let caCached = await identityCACached.get();
390
471
  if (!caCached) {
@@ -392,16 +473,13 @@ let identityCA = cache((domain: string) => {
392
473
  const keyPair = generateKeyPair();
393
474
  let caPublicKeyPart = getDomainPartFromPublicKey(keyPair.publicKey);
394
475
  let fullDomain = `${caPublicKeyPart}.${domain}`;
395
- if (!isNode()) {
396
- fullDomain = `${caPublicKeyPart}.${domain}`;
397
- }
398
476
 
399
- let value = createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
477
+ let value = await createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
400
478
 
401
479
  caCached = {
402
480
  domain: value.domain,
403
481
  certB64: value.cert.toString("base64"),
404
- keyB64: value.key.toString("base64"),
482
+ keyB64: keyPairKeyToBuffer(value.key).toString("base64"),
405
483
  };
406
484
  await identityCACached.set(caCached);
407
485
  }
@@ -417,6 +495,13 @@ let identityCA = cache((domain: string) => {
417
495
  return identityCA;
418
496
  });
419
497
 
498
+ function keyPairKeyToBuffer(key: Buffer | CryptoKey): Buffer {
499
+ if (key instanceof CryptoKey) {
500
+ throw new Error(`Cannot serialize a non-extractable CryptoKey`);
501
+ }
502
+ return key;
503
+ }
504
+
420
505
  // IMPORTANT! We do not embed any debug info in this domain. If we did, it would be useful,
421
506
  // but... potentally a security vulnerability, as if the debug info (such as a prefix)
422
507
  // is used to identify what a certificate is for, it would be easy for an attack to
@@ -425,9 +510,22 @@ let identityCA = cache((domain: string) => {
425
510
  // (and hopefully stored in a UI, showing IP, time, etc).
426
511
  export function createCertFromCA(config: {
427
512
  CAKeyPair: X509KeyPair;
428
- }): X509KeyPair {
429
- return measureBlock(function createCertFromCA() {
513
+ }): MaybePromise<X509KeyPair> {
514
+ return measureBlock(function createCertFromCA(): MaybePromise<X509KeyPair> {
430
515
  let { CAKeyPair } = config;
516
+ if (!isNode()) {
517
+ return (async () => {
518
+ const keyPair = await generateNonExtractableKeyPair();
519
+ let domainKeyPart = getDomainPartFromPublicKey(keyPair.publicKeyBytes);
520
+ let fullDomain = `${domainKeyPart}.${CAKeyPair.domain}`;
521
+ return await createX509({
522
+ domain: fullDomain,
523
+ issuer: CAKeyPair,
524
+ keyPair,
525
+ lifeSpan: timeInDay * 365 * 10,
526
+ });
527
+ })();
528
+ }
431
529
  const keyPair = generateKeyPair();
432
530
  let domainKeyPart = getDomainPartFromPublicKey(keyPair.publicKey);
433
531
  let fullDomain = `${domainKeyPart}.${config.CAKeyPair.domain}`;
@@ -440,8 +538,8 @@ export function createCertFromCA(config: {
440
538
  });
441
539
  }
442
540
 
443
- export function getMachineId(domainName: string) {
444
- return domainName.split(".").slice(-3).join(".");
541
+ export function getMachineId(domainNameOrNodeId: string, domain: string) {
542
+ return decodeNodeIdAssert(domainNameOrNodeId, domain, "allowMissingThreadId").machineId;
445
543
  }
446
544
 
447
545
  export type NodeIdParts = {
@@ -450,13 +548,18 @@ export type NodeIdParts = {
450
548
  domain: string;
451
549
  port: number;
452
550
  };
453
- export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
551
+ export function decodeNodeId(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts | undefined {
454
552
  let locationObj = getNodeIdLocation(nodeId);
455
553
  if (!locationObj) {
456
554
  return undefined;
457
555
  }
458
556
  let parts = locationObj.address.split(".");
459
- if (nodeId.startsWith("127-0-0-1.") && parts.length === 3) {
557
+ // NOTE: We have to only allow localhost domains on our own domain, as the underlying domain
558
+ // gets stripped when we're looking at the machineId. So if we allowed localhost domains on
559
+ // other domains, a server could trick us into connecting to it, and then once the connection
560
+ // is established, it could talk back and we would think it has a localhost machineId, which
561
+ // is implicitly trusted, which would then give it access to everything.
562
+ if (locationObj.address === `127-0-0-1.${domain}` && nodeId.includes(":")) {
460
563
  return {
461
564
  threadId: "",
462
565
  machineId: parts.at(-3) || "",
@@ -464,7 +567,8 @@ export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
464
567
  port: locationObj.port,
465
568
  };
466
569
  }
467
- if (parts.length < 4) {
570
+ let isValid = parts.length >= 4 || allowMissingThreadId && parts.length === 3;
571
+ if (!isValid) {
468
572
  return undefined;
469
573
  }
470
574
  return {
@@ -474,10 +578,10 @@ export function decodeNodeId(nodeId: string): NodeIdParts | undefined {
474
578
  port: locationObj.port,
475
579
  };
476
580
  }
477
- export function decodeNodeIdAssert(nodeId: string): NodeIdParts {
478
- let result = decodeNodeId(nodeId);
581
+ export function decodeNodeIdAssert(nodeId: string, domain: string, allowMissingThreadId?: "allowMissingThreadId"): NodeIdParts {
582
+ let result = decodeNodeId(nodeId, domain, allowMissingThreadId);
479
583
  if (!result) {
480
- throw new Error(`Invalid nodeId: ${nodeId}`);
584
+ throw new Error(`Invalid nodeId: ${JSON.stringify(nodeId)}`);
481
585
  }
482
586
  return result;
483
587
  }
@@ -486,12 +590,27 @@ export function encodeNodeId(parts: NodeIdParts) {
486
590
  }
487
591
 
488
592
  export async function setIdentityCARaw(domain: string, json: string) {
489
- let identityCACached = getIdentityStore(domain);
490
593
  let obj = JSON.parse(json) as {
491
594
  domain: string;
492
595
  certB64: string;
493
596
  keyB64: string;
494
597
  };
598
+ if (!isNode()) {
599
+ // Import into a non-extractable CryptoKey, so once stored the key can't be exfiltrated
600
+ let privateKey = await importEd25519PrivateKey(Buffer.from(obj.keyB64, "base64").toString());
601
+ let ca = {
602
+ domain: obj.domain,
603
+ cert: Buffer.from(obj.certB64, "base64"),
604
+ key: privateKey,
605
+ };
606
+ trustCertificate(ca.cert.toString());
607
+ identityCA(domain).set(ca);
608
+ getThreadKeyCertBase(domain).reset();
609
+ await getBrowserIdentityStore(domain).set({ domain: obj.domain, certB64: obj.certB64, privateKey });
610
+ resetAllNodeCallFactories();
611
+ return;
612
+ }
613
+ let identityCACached = getIdentityStore(domain);
495
614
  let ca = {
496
615
  domain: obj.domain,
497
616
  cert: Buffer.from(obj.certB64, "base64"),
@@ -504,6 +623,21 @@ export async function setIdentityCARaw(domain: string, json: string) {
504
623
  resetAllNodeCallFactories();
505
624
  }
506
625
 
626
+ async function importEd25519PrivateKey(pem: string): Promise<CryptoKey> {
627
+ let parsed = forge.ed25519.privateKeyFromPem(pem);
628
+ let bytes = Buffer.from(parsed.privateKeyBytes || parsed);
629
+ let seed = bytes;
630
+ if (seed.length === 64) {
631
+ // NaCl layout is seed || publicKey
632
+ seed = seed.subarray(0, 32);
633
+ }
634
+ if (seed.length !== 32) {
635
+ throw new Error(`Expected a 32 or 64 byte ed25519 private key, was ${bytes.length} bytes`);
636
+ }
637
+ let pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]);
638
+ return await globalThis.crypto.subtle.importKey("pkcs8", pkcs8, "Ed25519", false, ["sign"]);
639
+ }
640
+
507
641
  export async function loadIdentityCA(domain: string) {
508
642
  await identityCA(domain)();
509
643
  }
@@ -523,7 +657,14 @@ export function getIdentityCAPromise(domain: string): MaybePromise<X509KeyPair>
523
657
 
524
658
 
525
659
  export function getOwnMachineId(domain: string) {
526
- return getMachineId(getIdentityCA(domain).domain);
660
+ return getMachineId(getIdentityCA(domain).domain, domain);
661
+ }
662
+ export function getOwnThreadId(domain: string) {
663
+ let threadKeyCert = getThreadKeyCert(domain);
664
+ if (threadKeyCert instanceof Promise) {
665
+ throw new Error("Thread key cert is not yet loaded. Await getThreadKeyCert() in your startup before accessing the threadId");
666
+ }
667
+ return decodeNodeIdAssert(threadKeyCert.domain, domain).threadId;
527
668
  }
528
669
 
529
670
  /** Part of the machineId comes from the publicKey, so we can use it to verify */
@@ -533,7 +674,8 @@ export function verifyMachineIdForPublicKey(config: {
533
674
  }): boolean {
534
675
  let { machineId, publicKey } = config;
535
676
  let domainPart = getDomainPartFromPublicKey(publicKey);
536
- return machineId.split(".").at(-3) === domainPart;
677
+ // machineId is the bare key-hash label, but also accept legacy "hash.domain.tld" forms
678
+ return machineId.split(".")[0] === domainPart;
537
679
  }
538
680
 
539
681
  // NOTE: We don't have a cache per CA, as... the CA should be set first
@@ -14,4 +14,7 @@ declare module "node-forge" {
14
14
  static publicKeyToPem(key: Ed25519PublicKey): string;
15
15
  static publicKeyFromPem(pem: string): Ed25519PublicKey;
16
16
  }
17
+ namespace pki {
18
+ function getTBSCertificate(cert: pki.Certificate): asn1.Asn1;
19
+ }
17
20
  }
@@ -1,4 +1,8 @@
1
1
  import { MaybePromise } from "socket-function/src/types";
2
+ export declare function getIDBKeyStore<T>(appName: string, key: string): {
3
+ get(): Promise<T | undefined>;
4
+ set(value: T | undefined): Promise<void>;
5
+ };
2
6
  export declare function getKeyStore<T>(appName: string, key: string): {
3
7
  get(): MaybePromise<T | undefined>;
4
8
  set(value: T | null): MaybePromise<void>;
@@ -4,6 +4,42 @@ import os from "os";
4
4
  import { MaybePromise } from "socket-function/src/types";
5
5
  import { cache } from "socket-function/src/caching";
6
6
 
7
+ // Stores structured-cloneable values (including non-extractable CryptoKeys) in IndexedDB.
8
+ export function getIDBKeyStore<T>(appName: string, key: string): {
9
+ get(): Promise<T | undefined>;
10
+ set(value: T | undefined): Promise<void>;
11
+ } {
12
+ async function withStore<R>(mode: IDBTransactionMode, fnc: (store: IDBObjectStore) => IDBRequest<R>): Promise<R> {
13
+ let openReq = indexedDB.open(`keystore_${appName}`, 1);
14
+ openReq.onupgradeneeded = () => openReq.result.createObjectStore("kv");
15
+ let db = await new Promise<IDBDatabase>((resolve, reject) => {
16
+ openReq.onsuccess = () => resolve(openReq.result);
17
+ openReq.onerror = () => reject(openReq.error);
18
+ });
19
+ try {
20
+ let req = fnc(db.transaction("kv", mode).objectStore("kv"));
21
+ return await new Promise<R>((resolve, reject) => {
22
+ req.onsuccess = () => resolve(req.result);
23
+ req.onerror = () => reject(req.error);
24
+ });
25
+ } finally {
26
+ db.close();
27
+ }
28
+ }
29
+ return {
30
+ async get() {
31
+ return await withStore("readonly", store => store.get(key)) as T | undefined;
32
+ },
33
+ async set(value: T | undefined) {
34
+ if (!value) {
35
+ await withStore("readwrite", store => store.delete(key));
36
+ return;
37
+ }
38
+ await withStore("readwrite", store => store.put(value, key));
39
+ },
40
+ };
41
+ }
42
+
7
43
  export function getKeyStore<T>(appName: string, key: string): {
8
44
  get(): MaybePromise<T | undefined>;
9
45
  set(value: T | null): MaybePromise<void>;
@@ -0,0 +1 @@
1
+ export declare function ellipsize(text: string, maxLength: number): string;
@@ -0,0 +1,8 @@
1
+ module.allowclient = true;
2
+
3
+ export function ellipsize(text: string, maxLength: number): string {
4
+ if (text.length <= maxLength) {
5
+ return text;
6
+ }
7
+ return text.slice(0, maxLength - 3) + "...";
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.4.88",
3
+ "version": "1.5.1",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -53,6 +53,7 @@
53
53
  "acme-client": "^5.0.0",
54
54
  "js-sha256": "^0.11.1",
55
55
  "mobx": "^6.13.3",
56
+ "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
56
57
  "preact-old-types": "^10.28.1",
57
58
  "shell-quote": "^1.8.3",
58
59
  "socket-function": "^1.1.42",
package/yarn.lock CHANGED
@@ -1631,6 +1631,10 @@ node-forge@^1.3.1:
1631
1631
  resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.3.tgz#0ad80f6333b3a0045e827ac20b7f735f93716751"
1632
1632
  integrity sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==
1633
1633
 
1634
+ "node-forge@https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6":
1635
+ version "1.3.2-0"
1636
+ resolved "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6"
1637
+
1634
1638
  node-gyp-build-optional-packages@5.1.1:
1635
1639
  version "5.1.1"
1636
1640
  resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz#52b143b9dd77b7669073cbfe39e3f4118bfc603c"
@@ -1940,10 +1944,10 @@ slash@^3.0.0:
1940
1944
  resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
1941
1945
  integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
1942
1946
 
1943
- socket-function@^1.1.40:
1944
- version "1.1.40"
1945
- resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.40.tgz#0994aa018b3fcc64a131d29e5926377b7baff280"
1946
- integrity sha512-PtU8csq90nbjM/Jr/irRHzj2PojUgcMXTctfBMDEHJrLKmeOM4flf87svn0M8ad5iwNiSyav8SdxCzCl9IwZJw==
1947
+ socket-function@^1.1.42:
1948
+ version "1.1.47"
1949
+ resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.47.tgz#45cb508b4f402ae37c3053eb8b7cabe1cefaa011"
1950
+ integrity sha512-DT+TWWcr6GPQ6E/f8o3p+3UZlHfgnLPCJ6baSY+7jOOm5EdoPXcaCF5L4j4cna4OV4Tpw3ny85b4BqvixXP4sQ==
1947
1951
  dependencies:
1948
1952
  "@types/pako" "^2.0.3"
1949
1953
  "@types/ws" "^8.5.3"