@1auth/crypto 0.0.0-alpha.71 → 0.0.0-alpha.73
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.js +464 -429
- package/package.json +54 -54
package/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { promisify } from "node:util";
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
createHmac,
|
|
6
|
+
generateKeyPair as generateKeyPairCallback,
|
|
7
|
+
randomBytes,
|
|
8
|
+
randomInt,
|
|
9
|
+
sign as signCallback,
|
|
10
|
+
timingSafeEqual,
|
|
11
|
+
verify as verifyCallback,
|
|
13
12
|
} from "node:crypto";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
14
|
// https://github.com/napi-rs/node-rs/tree/main/packages/argon2
|
|
15
15
|
import { hash as secretHash, verify as secretVerify } from "@node-rs/argon2";
|
|
16
16
|
import { customAlphabet } from "nanoid";
|
|
@@ -20,63 +20,94 @@ const sign = promisify(signCallback);
|
|
|
20
20
|
const verify = promisify(verifyCallback);
|
|
21
21
|
|
|
22
22
|
const defaults = {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
symmetricEncryptionKey: undefined, // symmetricRandomEncryptionKey()
|
|
24
|
+
symmetricEncryptionAlgorithm: "chacha20-poly1305", // 2025-03: AES-256 GCM (aes-256-gcm) or ChaCha20-Poly1305 (chacha20-poly1305)
|
|
25
|
+
symmetricEncryptionEncoding: undefined, // https://nodejs.org/api/buffer.html#buffers-and-character-encodings
|
|
26
|
+
symmetricSignatureHashAlgorithm: undefined, // fallback to defaultHashAlgorithm
|
|
27
|
+
symmetricSignatureSecret: undefined, // symmetricRandomSignatureSecret()
|
|
28
|
+
symmetricSignatureEncoding: undefined, // fallback to defaultEncoding
|
|
29
|
+
asymmetricKeyNamedCurve: "P-384", // P-512
|
|
30
|
+
asymmetricSignatureHashAlgorithm: undefined, // fallback to defaultHashAlgorithm
|
|
31
|
+
asymmetricSignatureEncoding: undefined, // fallback to defaultEncoding
|
|
32
|
+
digestChecksumHashAlgorithm: undefined, // fallback to defaultHashAlgorithm
|
|
33
|
+
digestChecksumEncoding: undefined,
|
|
34
|
+
digestChecksumSalt: undefined, // randomChecksumSalt()
|
|
35
|
+
digestChecksumPepper: undefined, // randomChecksumPepper()
|
|
36
|
+
secretHashAlgorithm: "argon2id",
|
|
37
|
+
secretTimeCost: 3, // argon2id
|
|
38
|
+
secretMemoryCost: 2 ** 15, // argon2id
|
|
39
|
+
secretParallelism: 1, // argon2id
|
|
40
|
+
defaultEncoding: "base64",
|
|
41
|
+
defaultHashAlgorithm: "sha3-384",
|
|
38
42
|
};
|
|
39
43
|
const symmetricEncryptionEncodingLengths = {};
|
|
40
44
|
const options = {};
|
|
41
45
|
export default (opt = {}) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
46
|
+
Object.assign(options, defaults, opt);
|
|
47
|
+
|
|
48
|
+
// Check options, set defaults
|
|
49
|
+
if (!options.symmetricEncryptionKey) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
"@1auth/crypto symmetricEncryptionKey is empty, use a stored secret made from randomBytes(32) Encryption disabled.",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
options.symmetricEncryptionKey = makeOptionsBuffer(
|
|
55
|
+
options.symmetricEncryptionKey,
|
|
56
|
+
);
|
|
57
|
+
options.symmetricEncryptionEncoding ??= options.defaultEncoding;
|
|
58
|
+
options.symmetricSignatureHashAlgorithm ??= options.defaultHashAlgorithm;
|
|
59
|
+
if (!options.symmetricSignatureSecret) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
"@1auth/crypto symmetricSignatureSecret is empty, use a stored secret made from randomBytes(32) Signature disabled.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
options.symmetricSignatureSecret = makeOptionsBuffer(
|
|
65
|
+
options.symmetricSignatureSecret,
|
|
66
|
+
);
|
|
67
|
+
options.symmetricSignatureEncoding ??= options.defaultEncoding;
|
|
68
|
+
options.asymmetricSignatureHashAlgorithm ??= options.defaultHashAlgorithm;
|
|
69
|
+
options.asymmetricSignatureEncoding ??= options.defaultEncoding;
|
|
70
|
+
if (!options.digestChecksumSalt) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
"@1auth/crypto digestChecksumSalt is empty, use a stored secret made from randomBytes(32) Checksum salting disabled.",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
options.digestChecksumSalt = makeOptionsBuffer(options.digestChecksumSalt);
|
|
76
|
+
if (!options.digestChecksumPepper) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"@1auth/crypto digestChecksumPepper is empty, use a stored secret made from randomBytes(12) Checksum peppering disabled.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
options.digestChecksumPepper = makeOptionsBuffer(
|
|
82
|
+
options.digestChecksumPepper,
|
|
83
|
+
);
|
|
84
|
+
options.digestChecksumHashAlgorithm ??= options.defaultHashAlgorithm;
|
|
85
|
+
options.digestChecksumEncoding ??= options.defaultEncoding;
|
|
86
|
+
|
|
87
|
+
// Secrets
|
|
88
|
+
Object.assign(hashOptions, {
|
|
89
|
+
timeCost: options.secretTimeCost,
|
|
90
|
+
memoryCost: options.secretMemoryCost,
|
|
91
|
+
parallelism: options.secretParallelism,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Lengths
|
|
95
|
+
symmetricEncryptionEncodingLengths.iv = randomIV().toString(
|
|
96
|
+
options.symmetricEncryptionEncoding,
|
|
97
|
+
).length;
|
|
98
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag =
|
|
99
|
+
symmetricEncryptionEncodingLengths.iv +
|
|
100
|
+
randomBytes(16).toString(options.symmetricEncryptionEncoding).length;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const makeOptionsBuffer = (
|
|
104
|
+
value,
|
|
105
|
+
encoding = options.defaultEncoding,
|
|
106
|
+
) => {
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
return Buffer.from(value, encoding);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
80
111
|
};
|
|
81
112
|
|
|
82
113
|
export const getOptions = () => options;
|
|
@@ -100,8 +131,8 @@ export const getOptions = () => options;
|
|
|
100
131
|
// *** Helpers *** //
|
|
101
132
|
// Ref: https://therootcompany.com/blog/how-many-bits-of-entropy-per-character/
|
|
102
133
|
export const entropyToCharacterLength = (bits, characterPoolSize) => {
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
// bits*ln(2)/ln(characterPoolSize)
|
|
135
|
+
return Math.ceil((bits * Math.LN2) / Math.log(characterPoolSize));
|
|
105
136
|
};
|
|
106
137
|
/* export const characterLengthToEntropy = (
|
|
107
138
|
characterLength,
|
|
@@ -122,494 +153,498 @@ export const charactersAlphaNumeric = charactersAlpha + charactersNumeric;
|
|
|
122
153
|
export const charactersDistinguishable = "CDEHKMPRTUWXY012458";
|
|
123
154
|
|
|
124
155
|
const randomCharactersCache = {
|
|
125
|
-
|
|
156
|
+
charactersAlphaNumeric: customAlphabet(charactersAlphaNumeric),
|
|
126
157
|
};
|
|
127
158
|
export const randomCharacters = (
|
|
128
|
-
|
|
129
|
-
|
|
159
|
+
length,
|
|
160
|
+
characters = charactersAlphaNumeric,
|
|
130
161
|
) => {
|
|
131
|
-
|
|
132
|
-
|
|
162
|
+
randomCharactersCache[characters] ??= customAlphabet(characters);
|
|
163
|
+
return randomCharactersCache[characters](length);
|
|
133
164
|
};
|
|
134
165
|
|
|
135
166
|
export const randomAlphaNumeric = (characterLength) => {
|
|
136
|
-
|
|
167
|
+
return randomCharacters(characterLength, charactersAlphaNumeric);
|
|
137
168
|
};
|
|
138
169
|
|
|
139
170
|
export const randomNumeric = (characterLength) => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
171
|
+
let value = "";
|
|
172
|
+
for (let i = characterLength; i--; ) {
|
|
173
|
+
value += randomInt(9);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
145
176
|
};
|
|
146
177
|
|
|
147
178
|
// *** configs *** //
|
|
148
179
|
// Input: {id, prefix, entropy, characters, opt, expire}
|
|
149
180
|
// Output: {id, type, opt, expire, create, ...}
|
|
150
181
|
export const makeRandomConfigObject = ({
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
182
|
+
id,
|
|
183
|
+
prefix = "",
|
|
184
|
+
entropy = 64,
|
|
185
|
+
characters = charactersAlphaNumeric,
|
|
186
|
+
...params
|
|
156
187
|
} = {}) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
const minLength = entropyToCharacterLength(entropy, characters.length);
|
|
189
|
+
const config = {
|
|
190
|
+
id,
|
|
191
|
+
type: "id",
|
|
192
|
+
create: () => prefix + randomCharacters(minLength, characters),
|
|
193
|
+
...params,
|
|
194
|
+
};
|
|
195
|
+
return config;
|
|
165
196
|
};
|
|
166
197
|
|
|
167
198
|
// *** Digests *** //
|
|
168
199
|
export const randomChecksumSalt = () => {
|
|
169
|
-
|
|
200
|
+
return randomBytes(32); // 256 bits
|
|
170
201
|
};
|
|
171
202
|
export const randomChecksumPepper = () => {
|
|
172
|
-
|
|
203
|
+
return randomIV(); // 96
|
|
173
204
|
};
|
|
174
205
|
|
|
175
206
|
export const createSaltedValue = (value, { checksumSalt } = {}) => {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
checksumSalt ??= options.digestChecksumSalt;
|
|
208
|
+
if (!checksumSalt) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
const newValue = value + checksumSalt;
|
|
212
|
+
return newValue;
|
|
182
213
|
};
|
|
183
214
|
export const createPepperedValue = (
|
|
184
|
-
|
|
185
|
-
|
|
215
|
+
value,
|
|
216
|
+
{ checksumPepper, encryptionKey } = {},
|
|
186
217
|
) => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
218
|
+
checksumPepper ??= options.digestChecksumPepper;
|
|
219
|
+
encryptionKey ??= options.symmetricEncryptionKey;
|
|
220
|
+
if (!checksumPepper || !encryptionKey) {
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
const newValue = symmetricEncrypt(value, {
|
|
224
|
+
encryptionKey,
|
|
225
|
+
sub: "",
|
|
226
|
+
iv: checksumPepper,
|
|
227
|
+
});
|
|
228
|
+
return newValue;
|
|
198
229
|
};
|
|
199
230
|
|
|
200
231
|
export const createChecksum = (value, { hashAlgorithm, encoding } = {}) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
232
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
233
|
+
encoding ??= options.digestChecksumEncoding;
|
|
234
|
+
return createHash(hashAlgorithm).update(value).digest(encoding);
|
|
204
235
|
};
|
|
205
236
|
export const createSeasonedChecksum = (
|
|
206
|
-
|
|
207
|
-
|
|
237
|
+
value,
|
|
238
|
+
{ hashAlgorithm, encoding, checksumSalt, checksumPepper } = {},
|
|
208
239
|
) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
240
|
+
return createChecksum(
|
|
241
|
+
createPepperedValue(createSaltedValue(value, { checksumSalt }), {
|
|
242
|
+
checksumPepper,
|
|
243
|
+
}),
|
|
244
|
+
{
|
|
245
|
+
hashAlgorithm,
|
|
246
|
+
encoding,
|
|
247
|
+
},
|
|
248
|
+
);
|
|
218
249
|
};
|
|
219
250
|
|
|
220
251
|
export const createDigest = (value, { hashAlgorithm, encoding } = {}) => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
253
|
+
const checksum = createChecksum(value, { hashAlgorithm, encoding });
|
|
254
|
+
return `${hashAlgorithm}:${checksum}`;
|
|
224
255
|
};
|
|
225
256
|
export const createSaltedDigest = (
|
|
226
|
-
|
|
227
|
-
|
|
257
|
+
value,
|
|
258
|
+
{ hashAlgorithm, encoding, checksumSalt } = {},
|
|
228
259
|
) => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
260
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
261
|
+
const checksum = createChecksum(createSaltedValue(value, { checksumSalt }), {
|
|
262
|
+
hashAlgorithm,
|
|
263
|
+
encoding,
|
|
264
|
+
});
|
|
265
|
+
return `${hashAlgorithm}:${checksum}`;
|
|
235
266
|
};
|
|
236
267
|
export const createPepperedDigest = (
|
|
237
|
-
|
|
238
|
-
|
|
268
|
+
value,
|
|
269
|
+
{ hashAlgorithm, encoding, checksumPepper, encryptionKey } = {},
|
|
239
270
|
) => {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
271
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
272
|
+
const checksum = createChecksum(
|
|
273
|
+
createPepperedValue(value, { checksumPepper, encryptionKey }),
|
|
274
|
+
{
|
|
275
|
+
hashAlgorithm,
|
|
276
|
+
encoding,
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
return `${hashAlgorithm}:${checksum}`;
|
|
249
280
|
};
|
|
250
281
|
export const createSeasonedDigest = (
|
|
251
|
-
|
|
252
|
-
|
|
282
|
+
value,
|
|
283
|
+
{ hashAlgorithm, encoding, checksumSalt, checksumPepper, encryptionKey } = {},
|
|
253
284
|
) => {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
285
|
+
hashAlgorithm ??= options.digestChecksumHashAlgorithm;
|
|
286
|
+
const checksum = createSeasonedChecksum(value, {
|
|
287
|
+
hashAlgorithm,
|
|
288
|
+
encoding,
|
|
289
|
+
checksumSalt,
|
|
290
|
+
checksumPepper,
|
|
291
|
+
encryptionKey,
|
|
292
|
+
});
|
|
293
|
+
return `${hashAlgorithm}:${checksum}`;
|
|
263
294
|
};
|
|
264
295
|
|
|
265
296
|
// *** Hashing *** //
|
|
266
297
|
const hashOptions = {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
298
|
+
timeCost: 3, // Default 3
|
|
299
|
+
memoryCost: 2 ** 15, // Default 2 ** 12 = 4MB
|
|
300
|
+
parallelism: 1, // Default 1
|
|
301
|
+
saltLength: 16,
|
|
302
|
+
outputLen: 64, // hashLength: 128 // Default 32
|
|
303
|
+
algorithm: 2, // Default 2 = Argon2id
|
|
304
|
+
version: 1, // Default 1 = version 19
|
|
274
305
|
};
|
|
275
306
|
|
|
276
307
|
export const createSecretHash = async (value, options = hashOptions) => {
|
|
277
|
-
|
|
308
|
+
return secretHash(value, options);
|
|
278
309
|
};
|
|
279
310
|
|
|
280
311
|
export const verifySecretHash = async (hash, value) => {
|
|
281
|
-
|
|
312
|
+
return secretVerify(hash, value);
|
|
282
313
|
};
|
|
283
314
|
|
|
284
315
|
// *** Symmetric Encryption *** //
|
|
285
316
|
const authTagLength = 16;
|
|
286
317
|
|
|
287
318
|
export const symmetricRandomEncryptionKey = () => {
|
|
288
|
-
|
|
319
|
+
return randomBytes(32); // 256 bits
|
|
289
320
|
};
|
|
290
321
|
|
|
291
322
|
export const randomIV = () => {
|
|
292
|
-
|
|
323
|
+
return randomBytes(12); // 96 bits
|
|
293
324
|
};
|
|
294
325
|
|
|
295
326
|
export const symmetricGenerateEncryptionKey = (
|
|
296
|
-
|
|
297
|
-
|
|
327
|
+
sub,
|
|
328
|
+
{ encryptionKey, signatureSecret } = {},
|
|
298
329
|
) => {
|
|
299
|
-
|
|
300
|
-
|
|
330
|
+
encryptionKey ??= options.symmetricEncryptionKey;
|
|
331
|
+
signatureSecret ??= options.symemeticSignatureSecret;
|
|
301
332
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
333
|
+
const rowEncryptionKey = symmetricRandomEncryptionKey();
|
|
334
|
+
const rowEncryptedKey = symmetricEncrypt(rowEncryptionKey, {
|
|
335
|
+
encryptionKey,
|
|
336
|
+
signatureSecret,
|
|
337
|
+
sub,
|
|
338
|
+
});
|
|
339
|
+
return { encryptionKey: rowEncryptionKey, encryptedKey: rowEncryptedKey };
|
|
309
340
|
};
|
|
310
341
|
|
|
311
342
|
// sub add context to encryption
|
|
312
343
|
export const symmetricEncryptFields = (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
344
|
+
values,
|
|
345
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub },
|
|
346
|
+
fields = [],
|
|
316
347
|
) => {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
348
|
+
if (encryptedKey) {
|
|
349
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
350
|
+
signatureSecret,
|
|
351
|
+
sub,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (!encryptionKey) return values;
|
|
355
|
+
const encryptedValues = structuredClone(values);
|
|
356
|
+
for (const key of fields) {
|
|
357
|
+
encryptedValues[key] &&= symmetricEncrypt(encryptedValues[key], {
|
|
358
|
+
encryptionKey,
|
|
359
|
+
signatureSecret,
|
|
360
|
+
sub,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return encryptedValues;
|
|
333
364
|
};
|
|
334
365
|
|
|
335
366
|
export const symmetricEncrypt = (
|
|
336
|
-
|
|
337
|
-
|
|
367
|
+
data,
|
|
368
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding, iv },
|
|
338
369
|
) => {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
370
|
+
if (encryptedKey) {
|
|
371
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
372
|
+
signatureSecret,
|
|
373
|
+
sub,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (!encryptionKey || !data) return data;
|
|
377
|
+
decoding ??= "utf8";
|
|
378
|
+
encoding ??= options.symmetricEncryptionEncoding;
|
|
379
|
+
iv ??= randomIV();
|
|
380
|
+
const cipher = createCipheriv(
|
|
381
|
+
options.symmetricEncryptionAlgorithm,
|
|
382
|
+
encryptionKey,
|
|
383
|
+
iv,
|
|
384
|
+
{
|
|
385
|
+
authTagLength,
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
cipher.setAAD(sub);
|
|
389
|
+
const encryptedData =
|
|
390
|
+
cipher.update(data, decoding, encoding) + cipher.final(encoding);
|
|
391
|
+
const authTag = cipher.getAuthTag();
|
|
392
|
+
|
|
393
|
+
const encryptedDataPacket =
|
|
394
|
+
iv.toString(encoding) + authTag.toString(encoding) + encryptedData;
|
|
395
|
+
|
|
396
|
+
// add signature to end
|
|
397
|
+
return symmetricSignatureSign(encryptedDataPacket, { signatureSecret });
|
|
367
398
|
};
|
|
368
399
|
|
|
369
400
|
export const symmetricDecryptFields = (
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
401
|
+
encryptedValues,
|
|
402
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub },
|
|
403
|
+
fields = [],
|
|
373
404
|
) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
405
|
+
if (encryptedKey) {
|
|
406
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
407
|
+
signatureSecret,
|
|
408
|
+
sub,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (!encryptionKey) return encryptedValues;
|
|
412
|
+
const values = structuredClone(encryptedValues);
|
|
413
|
+
for (const key of fields) {
|
|
414
|
+
values[key] &&= symmetricDecrypt(values[key], {
|
|
415
|
+
encryptionKey,
|
|
416
|
+
signatureSecret,
|
|
417
|
+
sub,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
return values;
|
|
390
421
|
};
|
|
391
422
|
|
|
392
423
|
export const symmetricDecryptKey = (
|
|
393
|
-
|
|
394
|
-
|
|
424
|
+
encryptedKey,
|
|
425
|
+
{ sub, encryptionKey, signatureSecret } = {},
|
|
395
426
|
) => {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
427
|
+
encryptionKey ??= options.symmetricEncryptionKey;
|
|
428
|
+
signatureSecret ??= options.symemeticSignatureSecret;
|
|
429
|
+
return Buffer.from(
|
|
430
|
+
symmetricDecrypt(encryptedKey, {
|
|
431
|
+
encryptionKey,
|
|
432
|
+
signatureSecret,
|
|
433
|
+
sub,
|
|
434
|
+
encoding: options.symmetricEncryptionEncoding,
|
|
435
|
+
}),
|
|
436
|
+
options.symmetricEncryptionEncoding,
|
|
437
|
+
);
|
|
407
438
|
};
|
|
408
439
|
|
|
409
440
|
export const symmetricDecrypt = (
|
|
410
|
-
|
|
411
|
-
|
|
441
|
+
signedEncryptedDataPacket,
|
|
442
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding },
|
|
412
443
|
) => {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
444
|
+
if (encryptedKey) {
|
|
445
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
446
|
+
signatureSecret,
|
|
447
|
+
sub,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (!encryptionKey || !signedEncryptedDataPacket)
|
|
451
|
+
return signedEncryptedDataPacket;
|
|
452
|
+
decoding ??= options.symmetricEncryptionEncoding;
|
|
453
|
+
encoding ??= "utf8";
|
|
454
|
+
|
|
455
|
+
// remove signature when successful
|
|
456
|
+
const encryptedDataPacket = symmetricSignatureVerify(
|
|
457
|
+
signedEncryptedDataPacket,
|
|
458
|
+
{
|
|
459
|
+
signatureSecret,
|
|
460
|
+
},
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
if (encryptedDataPacket === false) {
|
|
464
|
+
throw new Error("Signature incorrect");
|
|
465
|
+
}
|
|
466
|
+
const iv = Buffer.from(
|
|
467
|
+
encryptedDataPacket.substring(0, symmetricEncryptionEncodingLengths.iv),
|
|
468
|
+
decoding,
|
|
469
|
+
);
|
|
470
|
+
const authTag = Buffer.from(
|
|
471
|
+
encryptedDataPacket.substring(
|
|
472
|
+
symmetricEncryptionEncodingLengths.iv,
|
|
473
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag,
|
|
474
|
+
),
|
|
475
|
+
decoding,
|
|
476
|
+
);
|
|
477
|
+
const encryptedData = Buffer.from(
|
|
478
|
+
encryptedDataPacket.substring(
|
|
479
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag,
|
|
480
|
+
),
|
|
481
|
+
decoding,
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
const decipher = createDecipheriv(
|
|
485
|
+
options.symmetricEncryptionAlgorithm,
|
|
486
|
+
encryptionKey,
|
|
487
|
+
iv,
|
|
488
|
+
{
|
|
489
|
+
authTagLength,
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
decipher.setAAD(sub);
|
|
493
|
+
|
|
494
|
+
decipher.setAuthTag(authTag);
|
|
495
|
+
const data =
|
|
496
|
+
decipher.update(encryptedData, decoding, encoding) +
|
|
497
|
+
decipher.final(encoding);
|
|
498
|
+
return data;
|
|
464
499
|
};
|
|
465
500
|
|
|
466
501
|
// *** Symmetric Signatures *** //
|
|
467
502
|
export const symmetricRandomSignatureSecret = () => {
|
|
468
|
-
|
|
503
|
+
return randomBytes(32); // 256 bits
|
|
469
504
|
};
|
|
470
505
|
|
|
471
506
|
export const symmetricGenerateSignatureSecret = () => {
|
|
472
|
-
|
|
473
|
-
|
|
507
|
+
const signatureSecret = symmetricRandomSignatureSecret();
|
|
508
|
+
return { signatureSecret };
|
|
474
509
|
};
|
|
475
510
|
|
|
476
511
|
export const symmetricSignatureSign = (
|
|
477
|
-
|
|
478
|
-
|
|
512
|
+
data,
|
|
513
|
+
{ hashAlgorithm, signatureSecret } = {},
|
|
479
514
|
) => {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
515
|
+
signatureSecret ??= options.symmetricSignatureSecret;
|
|
516
|
+
hashAlgorithm ??= options.symmetricSignatureHashAlgorithm;
|
|
517
|
+
const signature = createHmac(hashAlgorithm, signatureSecret)
|
|
518
|
+
.update(data)
|
|
519
|
+
.digest(options.symmetricSignatureEncoding)
|
|
520
|
+
.replace(/=+$/, "");
|
|
486
521
|
|
|
487
|
-
|
|
488
|
-
|
|
522
|
+
const signedData = `${data}.${signature}`;
|
|
523
|
+
return signedData;
|
|
489
524
|
};
|
|
490
525
|
|
|
491
526
|
export const symmetricSignatureVerify = (
|
|
492
|
-
|
|
493
|
-
|
|
527
|
+
signedData,
|
|
528
|
+
{ hashAlgorithm, signatureSecret } = {},
|
|
494
529
|
) => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
530
|
+
if (typeof signedData !== "string") return false;
|
|
531
|
+
let lastIndexOf = signedData.lastIndexOf(".");
|
|
532
|
+
// Test for unsigned
|
|
533
|
+
if (lastIndexOf < 0) {
|
|
534
|
+
lastIndexOf = signedData.length;
|
|
535
|
+
}
|
|
536
|
+
const data = signedData.substring(0, lastIndexOf);
|
|
537
|
+
const signedDataExpected = symmetricSignatureSign(data, {
|
|
538
|
+
hashAlgorithm,
|
|
539
|
+
signatureSecret,
|
|
540
|
+
});
|
|
541
|
+
return safeEqual(signedData, signedDataExpected) && data;
|
|
507
542
|
};
|
|
508
543
|
|
|
509
544
|
// Allow rotation of global encryption key, global signature secret, and row encryption key
|
|
510
545
|
export const symmetricRotation = (
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
546
|
+
oldEncryptedValues,
|
|
547
|
+
oldOptions, // { encryptionKey, signatureSecret, sub, decoding, encoding }, // old
|
|
548
|
+
oldFields,
|
|
549
|
+
newOptions, // { encryptionKey, signatureSecret, sub, decoding, encoding }, // new
|
|
550
|
+
newFields,
|
|
551
|
+
transform = (data) => {
|
|
552
|
+
return data;
|
|
553
|
+
},
|
|
519
554
|
) => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
555
|
+
if (oldOptions.sub !== newOptions.sub) throw new Error("Mismatching `sub`");
|
|
556
|
+
|
|
557
|
+
const oldEncryptedValuesClone = structuredClone(oldEncryptedValues);
|
|
558
|
+
// Don't use structuredClone, converts Buffer to Uint8Array ...
|
|
559
|
+
const oldOptionsClone = { ...oldOptions };
|
|
560
|
+
const newOptionsClone = { ...(newOptions ?? oldOptions) };
|
|
561
|
+
|
|
562
|
+
// decrypt old encryption key
|
|
563
|
+
const { encryptionKey: oldEncryptedKey } = oldEncryptedValuesClone;
|
|
564
|
+
oldEncryptedValuesClone.encryptionKey = undefined;
|
|
565
|
+
|
|
566
|
+
const oldEncryptionKey = symmetricDecryptKey(
|
|
567
|
+
oldEncryptedKey,
|
|
568
|
+
oldOptionsClone,
|
|
569
|
+
);
|
|
570
|
+
oldOptionsClone.encryptionKey = oldEncryptionKey;
|
|
571
|
+
|
|
572
|
+
// decrypt
|
|
573
|
+
const data = transform(
|
|
574
|
+
symmetricDecryptFields(
|
|
575
|
+
oldEncryptedValuesClone,
|
|
576
|
+
{ ...oldOptionsClone, encryptionKey: oldEncryptionKey },
|
|
577
|
+
oldFields,
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
// rotate encryptionKey
|
|
582
|
+
const { encryptionKey: newEncryptionKey, encryptedKey: newEncryptedKey } =
|
|
583
|
+
symmetricGenerateEncryptionKey(newOptionsClone.sub, newOptionsClone);
|
|
584
|
+
newOptionsClone.encryptionKey = newEncryptionKey;
|
|
585
|
+
|
|
586
|
+
// encrypt
|
|
587
|
+
const newEncryptedValues = symmetricEncryptFields(
|
|
588
|
+
data,
|
|
589
|
+
newOptionsClone,
|
|
590
|
+
newFields ?? oldFields,
|
|
591
|
+
);
|
|
592
|
+
newEncryptedValues.encryptionKey = newEncryptedKey;
|
|
593
|
+
|
|
594
|
+
return newEncryptedValues;
|
|
560
595
|
};
|
|
561
596
|
|
|
562
597
|
// *** Asymmetric Signatures *** //
|
|
563
598
|
// asymmetricKeyPairType
|
|
564
599
|
export const makeAsymmetricKeys = async () => {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
600
|
+
const { publicKey, privateKey } = await generateKeyPair("ec", {
|
|
601
|
+
namedCurve: options.asymmetricKeyNamedCurve,
|
|
602
|
+
paramEncoding: "named",
|
|
603
|
+
publicKeyEncoding: {
|
|
604
|
+
type: "spki",
|
|
605
|
+
format: "pem",
|
|
606
|
+
},
|
|
607
|
+
privateKeyEncoding: {
|
|
608
|
+
type: "sec1",
|
|
609
|
+
format: "pem",
|
|
610
|
+
// Encryption done at another level for consistency
|
|
611
|
+
// cipher: options.asymmetricEncryptionAlgorithm,
|
|
612
|
+
// passphrase: encryptionKey,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
return { publicKey, privateKey };
|
|
581
616
|
};
|
|
582
617
|
|
|
583
618
|
export const makeAsymmetricSignature = async (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
619
|
+
data,
|
|
620
|
+
privateKey,
|
|
621
|
+
{ hashAlgorithm } = {},
|
|
587
622
|
) => {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
623
|
+
hashAlgorithm ??= options.asymmetricSignatureHashAlgorithm;
|
|
624
|
+
return (await sign(hashAlgorithm, Buffer.from(data), privateKey)).toString(
|
|
625
|
+
options.asymmetricSignatureEncoding,
|
|
626
|
+
);
|
|
592
627
|
};
|
|
593
628
|
export const verifyAsymmetricSignature = async (
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
629
|
+
data,
|
|
630
|
+
publicKey,
|
|
631
|
+
signature,
|
|
632
|
+
{ hashAlgorithm } = {},
|
|
598
633
|
) => {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
634
|
+
hashAlgorithm ??= options.asymmetricSignatureHashAlgorithm;
|
|
635
|
+
return await verify(
|
|
636
|
+
hashAlgorithm,
|
|
637
|
+
Buffer.from(data),
|
|
638
|
+
publicKey,
|
|
639
|
+
Buffer.from(signature, options.asymmetricSignatureEncoding),
|
|
640
|
+
);
|
|
606
641
|
};
|
|
607
642
|
|
|
608
643
|
export const safeEqual = (input, expected) => {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
644
|
+
const bufferInput = Buffer.from(input);
|
|
645
|
+
const bufferExpected = Buffer.from(expected);
|
|
646
|
+
return (
|
|
647
|
+
bufferInput.length === bufferExpected.length &&
|
|
648
|
+
timingSafeEqual(bufferInput, bufferExpected)
|
|
649
|
+
);
|
|
615
650
|
};
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
2
|
+
"name": "@1auth/crypto",
|
|
3
|
+
"version": "0.0.0-alpha.73",
|
|
4
|
+
"description": "",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"engineStrict": true,
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"main": "./index.js",
|
|
14
|
+
"module": "./index.js",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"default": "./index.js"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"types": "index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"index.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "npm run test:unit",
|
|
30
|
+
"test:unit": "node --test"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"funding": {
|
|
34
|
+
"type": "github",
|
|
35
|
+
"url": "https://github.com/sponsors/willfarrell"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [],
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "1auth contributors",
|
|
40
|
+
"url": "https://github.com/willfarrell/1auth/graphs/contributors"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/willfarrell/1auth.git",
|
|
45
|
+
"directory": "packages/crypto"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/willfarrell/1auth/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/willfarrell/1auth",
|
|
51
|
+
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@node-rs/argon2": "2.0.2",
|
|
54
|
+
"nanoid": "5.1.5"
|
|
55
|
+
}
|
|
56
56
|
}
|