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 +36 -13
- package/misc/https/certs.d.ts +24 -13
- package/misc/https/certs.ts +192 -50
- 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/misc/strings.d.ts +1 -0
- package/misc/strings.ts +8 -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_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):
|
|
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(
|
|
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;
|
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_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):
|
|
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(
|
|
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;
|
package/misc/https/certs.ts
CHANGED
|
@@ -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 = "
|
|
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
|
-
|
|
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
|
|
115
|
+
// NOTE: nameConstraints require our forked node-forge:
|
|
103
116
|
// "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
130
|
-
return {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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(
|
|
444
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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;
|
package/misc/strings.ts
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sliftutils",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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"
|