sliftutils 1.5.0 → 1.5.2
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 +26 -9
- package/misc/https/certs.d.ts +19 -9
- package/misc/https/certs.ts +181 -27
- package/misc/https/node-forge-ed25519.d.ts +3 -0
- package/misc/https/persistentLocalStorage.d.ts +4 -0
- package/misc/https/persistentLocalStorage.ts +36 -0
- package/package.json +2 -1
- package/yarn.lock +8 -4
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 = "
|
|
75
|
+
export declare const identityStorageKey = "machineCA_12";
|
|
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,23 +100,24 @@ 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
|
|
110
|
+
key: string | Buffer | CryptoKey;
|
|
111
|
+
}, data: unknown) => MaybePromise<string>;
|
|
102
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
|
|
120
|
+
}): MaybePromise<X509KeyPair>;
|
|
111
121
|
export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
|
|
112
122
|
export type NodeIdParts = {
|
|
113
123
|
threadId: string;
|
|
@@ -129,7 +139,7 @@ declare module "sliftutils/misc/https/certs" {
|
|
|
129
139
|
machineId: string;
|
|
130
140
|
publicKey: Buffer;
|
|
131
141
|
}): boolean;
|
|
132
|
-
export declare function getThreadKeyCert(domain: string): X509KeyPair
|
|
142
|
+
export declare function getThreadKeyCert(domain: string): MaybePromise<X509KeyPair>;
|
|
133
143
|
export declare const createTestBrowserKeyCert: {
|
|
134
144
|
(): Promise<X509KeyPair>;
|
|
135
145
|
reset(): void;
|
|
@@ -213,11 +223,18 @@ declare module "sliftutils/misc/https/node-forge-ed25519" {
|
|
|
213
223
|
static publicKeyToPem(key: Ed25519PublicKey): string;
|
|
214
224
|
static publicKeyFromPem(pem: string): Ed25519PublicKey;
|
|
215
225
|
}
|
|
226
|
+
namespace pki {
|
|
227
|
+
function getTBSCertificate(cert: pki.Certificate): asn1.Asn1;
|
|
228
|
+
}
|
|
216
229
|
}
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
declare module "sliftutils/misc/https/persistentLocalStorage" {
|
|
220
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
|
+
};
|
|
221
238
|
export declare function getKeyStore<T>(appName: string, key: string): {
|
|
222
239
|
get(): MaybePromise<T | undefined>;
|
|
223
240
|
set(value: T | null): MaybePromise<void>;
|
package/misc/https/certs.d.ts
CHANGED
|
@@ -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 = "
|
|
8
|
+
export declare const identityStorageKey = "machineCA_12";
|
|
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,23 +33,24 @@ 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
|
|
43
|
+
key: string | Buffer | CryptoKey;
|
|
44
|
+
}, data: unknown) => MaybePromise<string>;
|
|
35
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
|
|
53
|
+
}): MaybePromise<X509KeyPair>;
|
|
44
54
|
export declare function getMachineId(domainNameOrNodeId: string, domain: string): string;
|
|
45
55
|
export type NodeIdParts = {
|
|
46
56
|
threadId: string;
|
|
@@ -62,7 +72,7 @@ export declare function verifyMachineIdForPublicKey(config: {
|
|
|
62
72
|
machineId: string;
|
|
63
73
|
publicKey: Buffer;
|
|
64
74
|
}): boolean;
|
|
65
|
-
export declare function getThreadKeyCert(domain: string): X509KeyPair
|
|
75
|
+
export declare function getThreadKeyCert(domain: string): MaybePromise<X509KeyPair>;
|
|
66
76
|
export declare const createTestBrowserKeyCert: {
|
|
67
77
|
(): Promise<X509KeyPair>;
|
|
68
78
|
reset(): void;
|
package/misc/https/certs.ts
CHANGED
|
@@ -18,7 +18,7 @@ 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
22
|
import { ellipsize } from "../strings";
|
|
23
23
|
|
|
24
24
|
setFlag(require, "node-forge", "allowclient", true);
|
|
@@ -28,14 +28,22 @@ const timeInDay = 1000 * 60 * 60 * 24;
|
|
|
28
28
|
|
|
29
29
|
export const CA_NOT_FOUND_ERROR = "18aa7318-f88f-4d2d-b41f-3daf4a433827";
|
|
30
30
|
|
|
31
|
-
export const identityStorageKey = "
|
|
31
|
+
export const identityStorageKey = "machineCA_12";
|
|
32
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 };
|
|
33
37
|
|
|
34
38
|
function getIdentityStore(domain: string) {
|
|
35
39
|
return getKeyStore<IdentityStorageType>(domain, identityStorageKey);
|
|
36
40
|
}
|
|
41
|
+
function getBrowserIdentityStore(domain: string) {
|
|
42
|
+
return getIDBKeyStore<BrowserIdentityStorageType>(domain, identityStorageKey);
|
|
43
|
+
}
|
|
37
44
|
|
|
38
|
-
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 };
|
|
39
47
|
|
|
40
48
|
export function getCommonName(cert: Buffer | string) {
|
|
41
49
|
let subject = new crypto.X509Certificate(cert).subject;
|
|
@@ -53,14 +61,18 @@ export function createX509(
|
|
|
53
61
|
keyPair: {
|
|
54
62
|
publicKey: forge.Ed25519PublicKey;
|
|
55
63
|
privateKey: forge.Ed25519PrivateKey;
|
|
56
|
-
} | forge.pki.KeyPair;
|
|
64
|
+
} | forge.pki.KeyPair | NonExtractableKeyPair;
|
|
57
65
|
}
|
|
58
|
-
): X509KeyPair {
|
|
59
|
-
return measureBlock(function createX509() {
|
|
66
|
+
): MaybePromise<X509KeyPair> {
|
|
67
|
+
return measureBlock(function createX509(): MaybePromise<X509KeyPair> {
|
|
60
68
|
let { domain, issuer, lifeSpan, keyPair } = config;
|
|
61
69
|
|
|
62
70
|
let certObj = forge.pki.createCertificate();
|
|
63
|
-
|
|
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
|
+
}
|
|
64
76
|
certObj.serialNumber = "01";
|
|
65
77
|
// Give it 5 minutes before now. If we give it too much time, it can look like the cert is really
|
|
66
78
|
// old, which will trigger various processes to try to get a fresher one (as if it lasts for
|
|
@@ -115,7 +127,18 @@ export function createX509(
|
|
|
115
127
|
certObj.setExtensions(extensions);
|
|
116
128
|
|
|
117
129
|
|
|
118
|
-
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
|
+
}
|
|
119
142
|
if (issuer === "self") {
|
|
120
143
|
certObj.sign(keyPair.privateKey as any, forge.md.sha256.create());
|
|
121
144
|
} else {
|
|
@@ -123,15 +146,44 @@ export function createX509(
|
|
|
123
146
|
}
|
|
124
147
|
});
|
|
125
148
|
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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();
|
|
133
168
|
});
|
|
134
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
|
+
}
|
|
135
187
|
export function privateKeyToPem(buffer: forge.pki.PrivateKey | forge.Ed25519PrivateKey) {
|
|
136
188
|
if ("privateKeyBytes" in buffer) {
|
|
137
189
|
return forge.ed25519.privateKeyToPem(buffer);
|
|
@@ -173,8 +225,13 @@ function isED25519(key: string | Buffer) {
|
|
|
173
225
|
}
|
|
174
226
|
|
|
175
227
|
// EQUIVALENT TO: `crypto.createSign("SHA256").update(JSON.stringify(payload)).sign(keyCert.key, "binary")`
|
|
176
|
-
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> {
|
|
177
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
|
+
}
|
|
178
235
|
if (isED25519(keyPair.key)) {
|
|
179
236
|
let privateKey = (forge.pki.ed25519 as any).privateKeyFromPem(keyPair.key.toString());
|
|
180
237
|
return privateKey.sign(dataStr);
|
|
@@ -201,6 +258,27 @@ function normalizeCertToPEM(PEMorDER: string | Buffer): string {
|
|
|
201
258
|
return "-----BEGIN CERTIFICATE-----\n" + PEMorDER + "\n-----END CERTIFICATE-----";
|
|
202
259
|
}
|
|
203
260
|
|
|
261
|
+
// Base32 (RFC 4648, lowercase, unpadded), as domain names are case-insensitive,
|
|
262
|
+
// which rules out base64/hex-with-case
|
|
263
|
+
const base32Alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
264
|
+
function encodeBase32(bytes: Buffer): string {
|
|
265
|
+
let result = "";
|
|
266
|
+
let bitCount = 0;
|
|
267
|
+
let value = 0;
|
|
268
|
+
for (let byte of bytes) {
|
|
269
|
+
value = (value << 8) | byte;
|
|
270
|
+
bitCount += 8;
|
|
271
|
+
while (bitCount >= 5) {
|
|
272
|
+
result += base32Alphabet[(value >>> (bitCount - 5)) & 31];
|
|
273
|
+
bitCount -= 5;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (bitCount > 0) {
|
|
277
|
+
result += base32Alphabet[(value << (5 - bitCount)) & 31];
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
|
|
204
282
|
function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } | forge.pki.KeyPair["publicKey"] | Buffer) {
|
|
205
283
|
let bytes: Buffer;
|
|
206
284
|
if ("publicKeyBytes" in publicKey) {
|
|
@@ -210,7 +288,7 @@ function getDomainPartFromPublicKey(publicKey: { publicKeyBytes: Buffer } | forg
|
|
|
210
288
|
} else {
|
|
211
289
|
bytes = Buffer.from(new Uint32Array((publicKey as any).n.data).buffer);
|
|
212
290
|
}
|
|
213
|
-
return "b" + Buffer.from(sha265.sha256.array(Buffer.from(bytes)))
|
|
291
|
+
return "b" + encodeBase32(Buffer.from(sha265.sha256.array(Buffer.from(bytes)))).slice(0, 20);
|
|
214
292
|
}
|
|
215
293
|
|
|
216
294
|
export function validateCACert(domain: string, cert: string | Buffer) {
|
|
@@ -384,6 +462,31 @@ export function generateTestCA(domain: string) {
|
|
|
384
462
|
|
|
385
463
|
let identityCA = cache((domain: string) => {
|
|
386
464
|
let identityCA = lazy((async (): Promise<X509KeyPair> => {
|
|
465
|
+
if (!isNode()) {
|
|
466
|
+
let store = getBrowserIdentityStore(domain);
|
|
467
|
+
let caCached = await store.get();
|
|
468
|
+
if (!caCached) {
|
|
469
|
+
console.log(`Generating new identity CA (non-extractable browser key)`);
|
|
470
|
+
const keyPair = await generateNonExtractableKeyPair();
|
|
471
|
+
let caPublicKeyPart = getDomainPartFromPublicKey(keyPair.publicKeyBytes);
|
|
472
|
+
let fullDomain = `${caPublicKeyPart}.${domain}`;
|
|
473
|
+
let value = await createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
|
|
474
|
+
caCached = {
|
|
475
|
+
domain: value.domain,
|
|
476
|
+
certB64: value.cert.toString("base64"),
|
|
477
|
+
privateKey: keyPair.privateKey,
|
|
478
|
+
};
|
|
479
|
+
await store.set(caCached);
|
|
480
|
+
}
|
|
481
|
+
let result = {
|
|
482
|
+
domain: caCached.domain,
|
|
483
|
+
cert: Buffer.from(caCached.certB64, "base64"),
|
|
484
|
+
key: caCached.privateKey,
|
|
485
|
+
};
|
|
486
|
+
trustCertificate(result.cert.toString());
|
|
487
|
+
identityCA.set(result);
|
|
488
|
+
return result;
|
|
489
|
+
}
|
|
387
490
|
let identityCACached = getIdentityStore(domain);
|
|
388
491
|
let caCached = await identityCACached.get();
|
|
389
492
|
if (!caCached) {
|
|
@@ -391,16 +494,13 @@ let identityCA = cache((domain: string) => {
|
|
|
391
494
|
const keyPair = generateKeyPair();
|
|
392
495
|
let caPublicKeyPart = getDomainPartFromPublicKey(keyPair.publicKey);
|
|
393
496
|
let fullDomain = `${caPublicKeyPart}.${domain}`;
|
|
394
|
-
if (!isNode()) {
|
|
395
|
-
fullDomain = `${caPublicKeyPart}.${domain}`;
|
|
396
|
-
}
|
|
397
497
|
|
|
398
|
-
let value = createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
|
|
498
|
+
let value = await createX509({ domain: fullDomain, issuer: "self", keyPair, lifeSpan: timeInDay * 365 * 20 });
|
|
399
499
|
|
|
400
500
|
caCached = {
|
|
401
501
|
domain: value.domain,
|
|
402
502
|
certB64: value.cert.toString("base64"),
|
|
403
|
-
keyB64: value.key.toString("base64"),
|
|
503
|
+
keyB64: keyPairKeyToBuffer(value.key).toString("base64"),
|
|
404
504
|
};
|
|
405
505
|
await identityCACached.set(caCached);
|
|
406
506
|
}
|
|
@@ -416,6 +516,13 @@ let identityCA = cache((domain: string) => {
|
|
|
416
516
|
return identityCA;
|
|
417
517
|
});
|
|
418
518
|
|
|
519
|
+
function keyPairKeyToBuffer(key: Buffer | CryptoKey): Buffer {
|
|
520
|
+
if (key instanceof CryptoKey) {
|
|
521
|
+
throw new Error(`Cannot serialize a non-extractable CryptoKey`);
|
|
522
|
+
}
|
|
523
|
+
return key;
|
|
524
|
+
}
|
|
525
|
+
|
|
419
526
|
// IMPORTANT! We do not embed any debug info in this domain. If we did, it would be useful,
|
|
420
527
|
// but... potentally a security vulnerability, as if the debug info (such as a prefix)
|
|
421
528
|
// is used to identify what a certificate is for, it would be easy for an attack to
|
|
@@ -424,9 +531,22 @@ let identityCA = cache((domain: string) => {
|
|
|
424
531
|
// (and hopefully stored in a UI, showing IP, time, etc).
|
|
425
532
|
export function createCertFromCA(config: {
|
|
426
533
|
CAKeyPair: X509KeyPair;
|
|
427
|
-
}): X509KeyPair {
|
|
428
|
-
return measureBlock(function createCertFromCA() {
|
|
534
|
+
}): MaybePromise<X509KeyPair> {
|
|
535
|
+
return measureBlock(function createCertFromCA(): MaybePromise<X509KeyPair> {
|
|
429
536
|
let { CAKeyPair } = config;
|
|
537
|
+
if (!isNode()) {
|
|
538
|
+
return (async () => {
|
|
539
|
+
const keyPair = await generateNonExtractableKeyPair();
|
|
540
|
+
let domainKeyPart = getDomainPartFromPublicKey(keyPair.publicKeyBytes);
|
|
541
|
+
let fullDomain = `${domainKeyPart}.${CAKeyPair.domain}`;
|
|
542
|
+
return await createX509({
|
|
543
|
+
domain: fullDomain,
|
|
544
|
+
issuer: CAKeyPair,
|
|
545
|
+
keyPair,
|
|
546
|
+
lifeSpan: timeInDay * 365 * 10,
|
|
547
|
+
});
|
|
548
|
+
})();
|
|
549
|
+
}
|
|
430
550
|
const keyPair = generateKeyPair();
|
|
431
551
|
let domainKeyPart = getDomainPartFromPublicKey(keyPair.publicKey);
|
|
432
552
|
let fullDomain = `${domainKeyPart}.${config.CAKeyPair.domain}`;
|
|
@@ -491,12 +611,27 @@ export function encodeNodeId(parts: NodeIdParts) {
|
|
|
491
611
|
}
|
|
492
612
|
|
|
493
613
|
export async function setIdentityCARaw(domain: string, json: string) {
|
|
494
|
-
let identityCACached = getIdentityStore(domain);
|
|
495
614
|
let obj = JSON.parse(json) as {
|
|
496
615
|
domain: string;
|
|
497
616
|
certB64: string;
|
|
498
617
|
keyB64: string;
|
|
499
618
|
};
|
|
619
|
+
if (!isNode()) {
|
|
620
|
+
// Import into a non-extractable CryptoKey, so once stored the key can't be exfiltrated
|
|
621
|
+
let privateKey = await importEd25519PrivateKey(Buffer.from(obj.keyB64, "base64").toString());
|
|
622
|
+
let ca = {
|
|
623
|
+
domain: obj.domain,
|
|
624
|
+
cert: Buffer.from(obj.certB64, "base64"),
|
|
625
|
+
key: privateKey,
|
|
626
|
+
};
|
|
627
|
+
trustCertificate(ca.cert.toString());
|
|
628
|
+
identityCA(domain).set(ca);
|
|
629
|
+
getThreadKeyCertBase(domain).reset();
|
|
630
|
+
await getBrowserIdentityStore(domain).set({ domain: obj.domain, certB64: obj.certB64, privateKey });
|
|
631
|
+
resetAllNodeCallFactories();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
let identityCACached = getIdentityStore(domain);
|
|
500
635
|
let ca = {
|
|
501
636
|
domain: obj.domain,
|
|
502
637
|
cert: Buffer.from(obj.certB64, "base64"),
|
|
@@ -509,13 +644,28 @@ export async function setIdentityCARaw(domain: string, json: string) {
|
|
|
509
644
|
resetAllNodeCallFactories();
|
|
510
645
|
}
|
|
511
646
|
|
|
647
|
+
async function importEd25519PrivateKey(pem: string): Promise<CryptoKey> {
|
|
648
|
+
let parsed = forge.ed25519.privateKeyFromPem(pem);
|
|
649
|
+
let bytes = Buffer.from(parsed.privateKeyBytes || parsed);
|
|
650
|
+
let seed = bytes;
|
|
651
|
+
if (seed.length === 64) {
|
|
652
|
+
// NaCl layout is seed || publicKey
|
|
653
|
+
seed = seed.subarray(0, 32);
|
|
654
|
+
}
|
|
655
|
+
if (seed.length !== 32) {
|
|
656
|
+
throw new Error(`Expected a 32 or 64 byte ed25519 private key, was ${bytes.length} bytes`);
|
|
657
|
+
}
|
|
658
|
+
let pkcs8 = Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), seed]);
|
|
659
|
+
return await globalThis.crypto.subtle.importKey("pkcs8", pkcs8, "Ed25519", false, ["sign"]);
|
|
660
|
+
}
|
|
661
|
+
|
|
512
662
|
export async function loadIdentityCA(domain: string) {
|
|
513
663
|
await identityCA(domain)();
|
|
514
664
|
}
|
|
515
665
|
export function getIdentityCA(domain: string): X509KeyPair {
|
|
516
666
|
let value = identityCA(domain)();
|
|
517
667
|
if (value instanceof Promise) {
|
|
518
|
-
throw new Error(
|
|
668
|
+
throw new Error(`Identity CA is not yet loaded. Call and wait for loadIdentityCA(${JSON.stringify(domain)}) in your startup before accessing the identity (or call getIdentityCAPromise(${JSON.stringify(domain)}))`);
|
|
519
669
|
}
|
|
520
670
|
return value;
|
|
521
671
|
}
|
|
@@ -531,7 +681,11 @@ export function getOwnMachineId(domain: string) {
|
|
|
531
681
|
return getMachineId(getIdentityCA(domain).domain, domain);
|
|
532
682
|
}
|
|
533
683
|
export function getOwnThreadId(domain: string) {
|
|
534
|
-
|
|
684
|
+
let threadKeyCert = getThreadKeyCert(domain);
|
|
685
|
+
if (threadKeyCert instanceof Promise) {
|
|
686
|
+
throw new Error(`Thread key cert is not yet loaded. Await getThreadKeyCert(${JSON.stringify(domain)}) in your startup before accessing the threadId`);
|
|
687
|
+
}
|
|
688
|
+
return decodeNodeIdAssert(threadKeyCert.domain, domain).threadId;
|
|
535
689
|
}
|
|
536
690
|
|
|
537
691
|
/** Part of the machineId comes from the publicKey, so we can use it to verify */
|
|
@@ -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>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sliftutils",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
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.
|
|
1944
|
-
version "1.1.
|
|
1945
|
-
resolved "https://registry.yarnpkg.com/socket-function/-/socket-function-1.1.
|
|
1946
|
-
integrity sha512-
|
|
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"
|