@1auth/crypto 0.0.0-alpha.7 → 0.0.0-alpha.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.js +553 -338
  2. package/package.json +7 -6
  3. package/LICENSE +0 -21
package/index.js CHANGED
@@ -1,30 +1,85 @@
1
- import { promisify } from 'node:util'
1
+ import { promisify } from "node:util";
2
2
  import {
3
3
  randomBytes,
4
- createHash as checksum,
4
+ randomInt,
5
+ createHash,
5
6
  createCipheriv,
6
7
  createDecipheriv,
8
+ createHmac,
9
+ timingSafeEqual,
7
10
  generateKeyPair as generateKeyPairCallback,
8
- sign,
9
- verify
10
- } from 'node:crypto'
11
+ sign as signCallback,
12
+ verify as verifyCallback,
13
+ } from "node:crypto";
11
14
  // https://github.com/napi-rs/node-rs/tree/main/packages/argon2
12
- import { hash as secretHash, verify as secretVerify } from '@node-rs/argon2'
13
-
14
- const options = {
15
- // randomBytes(32).toString('hex') // 256 bits
16
- encryptionSharedKey: '',
17
- encryptionMethod: 'chacha20-poly1305', // AES-256 GCM (aes-256-gcm) or ChaCha20-Poly1305 (chacha20-poly1305)
18
-
19
- digestAlgorithm: 'sha3-256',
20
- digestSalt: ''
21
- }
15
+ import { hash as secretHash, verify as secretVerify } from "@node-rs/argon2";
16
+ import { customAlphabet } from "nanoid";
17
+
18
+ const generateKeyPair = promisify(generateKeyPairCallback);
19
+ const sign = promisify(signCallback);
20
+ const verify = promisify(verifyCallback);
21
+
22
+ const defaults = {
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
+ defaultEncoding: "base64",
37
+ defaultHashAlgorithm: "sha3-384",
38
+ };
39
+ const symmetricEncryptionEncodingLengths = {};
40
+ const options = {};
41
+ export default (opt = {}) => {
42
+ Object.assign(options, defaults, opt);
43
+
44
+ // Check options, set defaults
45
+ if (!options.symmetricEncryptionKey) {
46
+ throw new Error(
47
+ "@1auth/crypto symmetricEncryptionKey is empty, use a stored secret made from randomBytes(32) Encryption disabled.",
48
+ );
49
+ }
50
+ options.symmetricEncryptionEncoding ??= options.defaultEncoding;
51
+ options.symmetricSignatureHashAlgorithm ??= options.defaultHashAlgorithm;
52
+ if (!options.symmetricSignatureSecret) {
53
+ throw new Error(
54
+ "@1auth/crypto symmetricSignatureSecret is empty, use a stored secret made from randomBytes(32) Signature disabled.",
55
+ );
56
+ }
57
+ options.symmetricSignatureEncoding ??= options.defaultEncoding;
58
+ options.asymmetricSignatureHashAlgorithm ??= options.defaultHashAlgorithm;
59
+ options.asymmetricSignatureEncoding ??= options.defaultEncoding;
60
+ if (!options.digestChecksumSalt) {
61
+ throw new Error(
62
+ "@1auth/crypto digestChecksumSalt is empty, use a stored secret made from randomBytes(32) Checksum salting disabled.",
63
+ );
64
+ }
65
+ if (!options.digestChecksumPepper) {
66
+ throw new Error(
67
+ "@1auth/crypto digestChecksumPepper is empty, use a stored secret made from randomBytes(12) Checksum peppering disabled.",
68
+ );
69
+ }
70
+ options.digestChecksumHashAlgorithm ??= options.defaultHashAlgorithm;
71
+ options.digestChecksumEncoding ??= options.defaultEncoding;
22
72
 
23
- export default (params) => {
24
- Object.assign(options, params)
25
- }
73
+ // Lengths
74
+ symmetricEncryptionEncodingLengths.iv = randomIV().toString(
75
+ options.symmetricEncryptionEncoding,
76
+ ).length;
77
+ symmetricEncryptionEncodingLengths.ivAndAuthTag =
78
+ symmetricEncryptionEncodingLengths.iv +
79
+ randomBytes(16).toString(options.symmetricEncryptionEncoding).length;
80
+ };
26
81
 
27
- const generateKeyPair = promisify(generateKeyPairCallback)
82
+ export const getOptions = () => options;
28
83
 
29
84
  // *** entropy *** //
30
85
  // export const characterPoolSize = (value) => {
@@ -42,359 +97,519 @@ const generateKeyPair = promisify(generateKeyPairCallback)
42
97
  // return max - min
43
98
  // }
44
99
 
45
- // *** Random generators *** //
46
- export const characterPoolSize = {
47
- keyboard: 94, // (26 + 10 + 11) * 2
48
- alphaNumeric: 62, // (26 + 10) * 2
49
- base64: 64, // (26 + 10) * 2 + 2
50
- hex: 16,
51
- numeric: 10
52
- }
53
- // Alt: https://github.com/sindresorhus/crypto-random-string/blob/main/core.js
54
- export const randomAlphaNumeric = (bits) => {
55
- const characterLength = entropyToCharacterLength(
56
- bits,
57
- characterPoolSize.alphaNumeric
58
- )
59
- return randomBytes(characterLength * 2)
60
- .toString('base64')
61
- .replace(/[^a-zA-Z0-9]/g, '')
62
- .substring(0, characterLength)
63
- }
64
-
65
- export const randomBase64 = (bits) => {
66
- const characterLength = entropyToCharacterLength(
67
- bits,
68
- characterPoolSize.base64
69
- )
70
- return randomBytes(characterLength + 1)
71
- .toString('base64')
72
- .replace('=', '')
73
- }
74
-
75
- export const randomNumeric = (bits) => {
76
- const characterLength = entropyToCharacterLength(
77
- bits,
78
- characterPoolSize.numeric
79
- )
80
- const max = 10 ** characterLength
81
- let value = Math.floor(Math.random() * max).toString()
82
- if (value === max.toString()) {
83
- return randomNumeric(bits)
84
- }
85
- while (value.length < characterLength) value = '0' + value
86
- return value
87
- }
88
-
89
- export const randomHex = (bits) => {
90
- const characterLength = entropyToCharacterLength(bits, characterPoolSize.hex)
91
- return randomBytes(characterLength).toString('hex')
92
- }
93
-
94
- // *** configs *** //
95
- export const randomId = {
96
- type: 'id',
97
- entropy: 64,
98
- charPool: characterPoolSize.alphaNumeric,
99
- create: async () => randomAlphaNumeric(randomId.entropy)
100
- }
101
-
102
- export const subject = {
103
- type: 'id',
104
- entropy: 64,
105
- charPool: characterPoolSize.alphaNumeric,
106
- create: async () => randomAlphaNumeric(subject.entropy)
107
- }
108
-
109
- export const session = {
110
- type: 'id',
111
- entropy: 128, // ASVS 3.2.2
112
- charPool: characterPoolSize.alphaNumeric,
113
- expire: 15 * 60,
114
- create: async () => randomAlphaNumeric(session.entropy)
115
- }
116
-
117
- export const passwordSecret = {
118
- type: 'secret',
119
- entropy: 64,
120
- charPool: characterPoolSize.keyboard,
121
- otp: false,
122
- encode: async (value, encryptedKey, sub) =>
123
- createSecretHash(value).then((hash) => encrypt(hash, encryptedKey, sub)),
124
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub),
125
- verify: async (value, hash) => verifySecretHash(hash, value)
126
- }
127
-
128
- // aka Password/Credential Recovery
129
- export const passwordToken = {
130
- type: 'token',
131
- entropy: 20,
132
- charPool: characterPoolSize.numeric,
133
- otp: true,
134
- expire: 30,
135
- create: async () => randomNumeric(passwordToken.entropy),
136
- encode: async (value, encryptedKey, sub) =>
137
- createSecretHash(value).then((hash) => encrypt(hash, encryptedKey, sub)),
138
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub),
139
- verify: async (value, hash) => verifySecretHash(hash, value)
140
- }
141
-
142
- export const oneTimeSecret = {
143
- type: 'secret',
144
- entropy: 64, //
145
- charPool: characterPoolSize.base64,
146
- otp: false,
147
- expire: null,
148
- create: async () => randomBase64(oneTimeSecret.entropy),
149
- encode: async (value, encryptedKey, sub) => encrypt(value, encryptedKey, sub),
150
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub)
151
- // create, hash, verify to be handled within
152
- }
153
- export const oneTimeToken = {
154
- type: 'token',
155
- entropy: 20,
156
- charPool: characterPoolSize.numeric,
157
- otp: true,
158
- expire: 30
159
- // create, hash, verify to be handled within
160
- }
161
-
162
- // aka lookup secret
163
- export const recoveryCode = {
164
- type: 'secret',
165
- entropy: 112,
166
- charPool: characterPoolSize.alphaNumeric,
167
- otp: true,
168
- create: async () => randomAlphaNumeric(recoveryCode.entropy),
169
- encode: async (value, encryptedKey, sub) =>
170
- createSecretHash(value).then((hash) => encrypt(hash, encryptedKey, sub)),
171
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub),
172
- verify: async (value, hash) => verifySecretHash(hash, value)
173
- }
174
-
175
- export const outOfBandToken = {
176
- type: 'token',
177
- entropy: 20,
178
- charPool: characterPoolSize.numeric,
179
- otp: true,
180
- expire: 10 * 60,
181
- create: async () => randomNumeric(outOfBandToken.entropy),
182
- encode: async (value, encryptedKey, sub) =>
183
- createSecretHash(value).then((hash) => encrypt(hash, encryptedKey, sub)),
184
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub),
185
- verify: async (value, hash) => verifySecretHash(hash, value)
186
- }
187
-
188
- export const accessToken = {
189
- type: 'secret',
190
- entropy: 112,
191
- charPool: characterPoolSize.alphaNumeric,
192
- otp: false,
193
- expire: 30 * 24 * 60 * 60, // allow override from user
194
- create: async () => randomAlphaNumeric(accessToken.entropy),
195
- encode: async (value, encryptedKey, sub) =>
196
- createSecretHash(value).then((hash) => encrypt(hash, encryptedKey, sub)),
197
- decode: async (value, encryptedKey, sub) => decrypt(value, encryptedKey, sub),
198
- verify: async (value, hash) => verifySecretHash(hash, value)
199
- }
200
-
201
100
  // *** Helpers *** //
202
- export const characterLengthToEntropy = (
101
+ // Ref: https://therootcompany.com/blog/how-many-bits-of-entropy-per-character/
102
+ export const entropyToCharacterLength = (bits, characterPoolSize) => {
103
+ // bits*ln(2)/ln(characterPoolSize)
104
+ return Math.ceil((bits * Math.LN2) / Math.log(characterPoolSize));
105
+ };
106
+ /* export const characterLengthToEntropy = (
203
107
  characterLength,
204
- characterPoolSize
108
+ characterPoolSize,
205
109
  ) => {
206
110
  // log_2(characterPoolSize^characterLength)
207
- return Math.round(Math.log2(characterPoolSize ** characterLength))
208
- }
111
+ return Math.floor(Math.log2(characterPoolSize ** characterLength));
112
+ }; */
209
113
 
210
- export const entropyToCharacterLength = (bits, characterPoolSize) => {
211
- // bits*ln(2)/ln(characterPoolSize)
212
- return Math.round((bits * Math.LN2) / Math.log(characterPoolSize))
213
- }
114
+ // *** Random generators *** //
115
+ export { randomBytes, randomInt, randomUUID } from "node:crypto";
116
+
117
+ export const charactersNumeric = "0123456789";
118
+ export const charactersAlphaUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
119
+ export const charactersAlphaLower = "abcdefghijklmnopqrstuvwxyz";
120
+ export const charactersAlpha = charactersAlphaUpper + charactersAlphaLower;
121
+ export const charactersAlphaNumeric = charactersAlpha + charactersNumeric;
122
+ export const charactersDistinguishable = "CDEHKMPRTUWXY012458";
123
+
124
+ const randomCharactersCache = {
125
+ charactersAlphaNumeric: customAlphabet(charactersAlphaNumeric),
126
+ };
127
+ export const randomCharacters = (
128
+ length,
129
+ characters = charactersAlphaNumeric,
130
+ ) => {
131
+ randomCharactersCache[characters] ??= customAlphabet(characters);
132
+ return randomCharactersCache[characters](length);
133
+ };
134
+
135
+ export const randomAlphaNumeric = (characterLength) => {
136
+ return randomCharacters(characterLength, charactersAlphaNumeric);
137
+ };
138
+
139
+ export const randomNumeric = (characterLength) => {
140
+ let value = "";
141
+ for (let i = characterLength; i--; ) {
142
+ value += randomInt(9);
143
+ }
144
+ return value;
145
+ };
146
+
147
+ // *** configs *** //
148
+ // Input: {id, prefix, entropy, characters, opt, expire}
149
+ // Output: {id, type, opt, expire, create, ...}
150
+ export const makeRandomConfigObject = ({
151
+ id,
152
+ prefix = "",
153
+ entropy = 64,
154
+ characters = charactersAlphaNumeric,
155
+ ...params
156
+ } = {}) => {
157
+ const minLength = entropyToCharacterLength(entropy, characters.length);
158
+ const config = {
159
+ id,
160
+ type: "id",
161
+ create: () => prefix + randomCharacters(minLength, characters),
162
+ ...params,
163
+ };
164
+ return config;
165
+ };
214
166
 
215
167
  // *** Digests *** //
216
- export const createDigest = async (value, { algorithm, salt } = {}) => {
217
- algorithm ??= options.digestAlgorithm
218
- salt ??= options.digestSalt
219
- const hash = checksum(algorithm)
220
- .update(value + salt)
221
- .digest('hex')
222
- return `${algorithm}:${hash}`
223
- }
168
+ export const randomChecksumSalt = () => {
169
+ return randomBytes(32); // 256 bits
170
+ };
171
+ export const randomChecksumPepper = () => {
172
+ return randomIV(); // 96
173
+ };
174
+
175
+ export const createSaltedValue = (value, { checksumSalt } = {}) => {
176
+ checksumSalt ??= options.digestChecksumSalt;
177
+ if (!checksumSalt) {
178
+ return value;
179
+ }
180
+ const newValue = value + checksumSalt;
181
+ return newValue;
182
+ };
183
+ export const createPepperedValue = (
184
+ value,
185
+ { checksumPepper, encryptionKey } = {},
186
+ ) => {
187
+ checksumPepper ??= options.digestChecksumPepper;
188
+ encryptionKey ??= options.symmetricEncryptionKey;
189
+ if (!checksumPepper || !encryptionKey) {
190
+ return value;
191
+ }
192
+ const newValue = symmetricEncrypt(value, {
193
+ encryptionKey,
194
+ sub: "",
195
+ iv: checksumPepper,
196
+ });
197
+ return newValue;
198
+ };
199
+
200
+ export const createChecksum = (value, { hashAlgorithm, encoding } = {}) => {
201
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
202
+ encoding ??= options.digestChecksumEncoding;
203
+ return createHash(hashAlgorithm).update(value).digest(encoding);
204
+ };
205
+ export const createSeasonedChecksum = (
206
+ value,
207
+ { hashAlgorithm, encoding, checksumSalt, checksumPepper } = {},
208
+ ) => {
209
+ return createChecksum(
210
+ createPepperedValue(createSaltedValue(value, { checksumSalt }), {
211
+ checksumPepper,
212
+ }),
213
+ {
214
+ hashAlgorithm,
215
+ encoding,
216
+ },
217
+ );
218
+ };
219
+
220
+ export const createDigest = (value, { hashAlgorithm, encoding } = {}) => {
221
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
222
+ const checksum = createChecksum(value, { hashAlgorithm, encoding });
223
+ return `${hashAlgorithm}:${checksum}`;
224
+ };
225
+ export const createSaltedDigest = (
226
+ value,
227
+ { hashAlgorithm, encoding, checksumSalt } = {},
228
+ ) => {
229
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
230
+ const checksum = createChecksum(createSaltedValue(value, { checksumSalt }), {
231
+ hashAlgorithm,
232
+ encoding,
233
+ });
234
+ return `${hashAlgorithm}:${checksum}`;
235
+ };
236
+ export const createPepperedDigest = (
237
+ value,
238
+ { hashAlgorithm, encoding, checksumPepper, encryptionKey } = {},
239
+ ) => {
240
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
241
+ const checksum = createChecksum(
242
+ createPepperedValue(value, { checksumPepper, encryptionKey }),
243
+ {
244
+ hashAlgorithm,
245
+ encoding,
246
+ },
247
+ );
248
+ return `${hashAlgorithm}:${checksum}`;
249
+ };
250
+ export const createSeasonedDigest = (
251
+ value,
252
+ { hashAlgorithm, encoding, checksumSalt, checksumPepper, encryptionKey } = {},
253
+ ) => {
254
+ hashAlgorithm ??= options.digestChecksumHashAlgorithm;
255
+ const checksum = createSeasonedChecksum(value, {
256
+ hashAlgorithm,
257
+ encoding,
258
+ checksumSalt,
259
+ checksumPepper,
260
+ encryptionKey,
261
+ });
262
+ return `${hashAlgorithm}:${checksum}`;
263
+ };
224
264
 
225
265
  // *** Hashing *** //
226
266
  const hashOptions = {
227
- timeCost: 3,
228
- memoryCost: 2 ** 16,
267
+ timeCost: 3, // Default 3
268
+ memoryCost: 2 ** 15, // Default 2 ** 12 = 4MB
229
269
  saltLength: 16,
230
- parallelism: 1,
231
- outputLen: 64, // hashLength: 128
232
- algorithm: 2,
233
- version: 1
234
- }
270
+ parallelism: 1, // Default 1
271
+ outputLen: 64, // hashLength: 128 // Default 32
272
+ algorithm: 2, // Default 2 = Argon2id
273
+ version: 1, // Default 1 = version 19
274
+ };
235
275
 
236
276
  export const createSecretHash = async (value, options = hashOptions) => {
237
- return secretHash(value, options)
238
- }
277
+ return secretHash(value, options);
278
+ };
239
279
 
240
280
  export const verifySecretHash = async (hash, value) => {
241
- return secretVerify(hash, value)
242
- }
281
+ return secretVerify(hash, value);
282
+ };
243
283
 
244
- // *** Encryption *** //
245
- const authTagLength = 16
284
+ // *** Symmetric Encryption *** //
285
+ const authTagLength = 16;
246
286
 
247
- export const makeSymetricKey = (assocData) => {
248
- if (!options.encryptionSharedKey) {
249
- return { encryptionKey: '', encryptedKey: '' }
250
- }
251
- const encryptionKey = randomBytes(32)
252
- const encryptedKey = __encrypt(
287
+ export const symmetricRandomEncryptionKey = () => {
288
+ return randomBytes(32); // 256 bits
289
+ };
290
+
291
+ export const randomIV = () => {
292
+ return randomBytes(12); // 96 bits
293
+ };
294
+
295
+ export const symmetricGenerateEncryptionKey = (
296
+ sub,
297
+ { encryptionKey, signatureSecret } = {},
298
+ ) => {
299
+ encryptionKey ??= options.symmetricEncryptionKey;
300
+ signatureSecret ??= options.symemeticSignatureSecret;
301
+
302
+ const rowEncryptionKey = symmetricRandomEncryptionKey();
303
+ const rowEncryptedKey = symmetricEncrypt(rowEncryptionKey, {
253
304
  encryptionKey,
254
- Buffer.from(options.encryptionSharedKey, 'hex'),
255
- assocData,
256
- 'hex',
257
- 'hex'
258
- )
259
- return { encryptionKey, encryptedKey }
260
- }
261
-
262
- export const encrypt = (data, encryptedKey, assocData) => {
263
- if (!encryptedKey) return data
264
-
265
- const encryptionKey = __decrypt(
266
- encryptedKey,
267
- Buffer.from(options.encryptionSharedKey, 'hex'),
268
- assocData,
269
- 'hex',
270
- 'hex'
271
- )
272
- return __encrypt(
273
- data,
274
- Buffer.from(encryptionKey, 'hex'),
275
- assocData,
276
- 'utf8',
277
- 'hex'
278
- )
279
- }
280
-
281
- const __encrypt = (
305
+ signatureSecret,
306
+ sub,
307
+ });
308
+ return { encryptionKey: rowEncryptionKey, encryptedKey: rowEncryptedKey };
309
+ };
310
+
311
+ // sub add context to encryption
312
+ export const symmetricEncryptFields = (
313
+ values,
314
+ { encryptedKey, encryptionKey, signatureSecret, sub },
315
+ fields = [],
316
+ ) => {
317
+ if (encryptedKey) {
318
+ encryptionKey ??= symmetricDecryptKey(encryptedKey, {
319
+ signatureSecret,
320
+ sub,
321
+ });
322
+ }
323
+ if (!encryptionKey) return values;
324
+ const encryptedValues = structuredClone(values);
325
+ for (const key of fields) {
326
+ encryptedValues[key] &&= symmetricEncrypt(encryptedValues[key], {
327
+ encryptionKey,
328
+ signatureSecret,
329
+ sub,
330
+ });
331
+ }
332
+ return encryptedValues;
333
+ };
334
+
335
+ export const symmetricEncrypt = (
282
336
  data,
283
- encryptionKey,
284
- assocData,
285
- decoding = 'utf8',
286
- encoding = 'hex'
337
+ { encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding, iv },
287
338
  ) => {
288
- const iv = randomBytes(12) // 96 bits
289
- const cipher = createCipheriv(options.encryptionMethod, encryptionKey, iv, {
290
- authTagLength
291
- })
292
- cipher.setAAD(Buffer.from(assocData, 'utf8'))
339
+ if (encryptedKey) {
340
+ encryptionKey ??= symmetricDecryptKey(encryptedKey, {
341
+ signatureSecret,
342
+ sub,
343
+ });
344
+ }
345
+ if (!encryptionKey || !data) return data;
346
+ decoding ??= "utf8";
347
+ encoding ??= options.symmetricEncryptionEncoding;
348
+ iv ??= randomIV();
349
+ const cipher = createCipheriv(
350
+ options.symmetricEncryptionAlgorithm,
351
+ encryptionKey,
352
+ iv,
353
+ {
354
+ authTagLength,
355
+ },
356
+ );
357
+ cipher.setAAD(sub);
293
358
  const encryptedData =
294
- cipher.update(data, decoding, encoding) + cipher.final(encoding)
295
- const authTag = cipher.getAuthTag()
296
- return (
297
- iv.toString(encoding) + // 24 char
298
- authTag.toString(encoding) + // 32 char
299
- encryptedData
300
- )
301
- }
302
-
303
- export const decrypt = (encryptedData, encryptedKey, assocData) => {
304
- if (!options.encryptionSharedKey || !encryptedKey) return encryptedData
305
- const encryptionKey = __decrypt(
306
- encryptedKey,
307
- Buffer.from(options.encryptionSharedKey, 'hex'),
308
- assocData,
309
- 'hex',
310
- 'hex'
311
- )
312
- const data = __decrypt(
313
- encryptedData,
314
- Buffer.from(encryptionKey, 'hex'),
315
- assocData,
316
- 'hex',
317
- 'utf8'
318
- )
319
- return data
320
- }
321
-
322
- const __decrypt = (
323
- data,
324
- encryptionKey,
325
- assocData,
326
- decoding = 'hex',
327
- encoding = 'utf8'
359
+ cipher.update(data, decoding, encoding) + cipher.final(encoding);
360
+ const authTag = cipher.getAuthTag();
361
+
362
+ const encryptedDataPacket =
363
+ iv.toString(encoding) + authTag.toString(encoding) + encryptedData;
364
+
365
+ // add signature to end
366
+ return symmetricSignatureSign(encryptedDataPacket, { signatureSecret });
367
+ };
368
+
369
+ export const symmetricDecryptFields = (
370
+ encryptedValues,
371
+ { encryptedKey, encryptionKey, signatureSecret, sub },
372
+ fields = [],
373
+ ) => {
374
+ if (encryptedKey) {
375
+ encryptionKey ??= symmetricDecryptKey(encryptedKey, {
376
+ signatureSecret,
377
+ sub,
378
+ });
379
+ }
380
+ if (!encryptionKey) return encryptedValues;
381
+ const values = structuredClone(encryptedValues);
382
+ for (const key of fields) {
383
+ values[key] &&= symmetricDecrypt(values[key], {
384
+ encryptionKey,
385
+ signatureSecret,
386
+ sub,
387
+ });
388
+ }
389
+ return values;
390
+ };
391
+
392
+ export const symmetricDecryptKey = (
393
+ encryptedKey,
394
+ { sub, encryptionKey, signatureSecret } = {},
395
+ ) => {
396
+ encryptionKey ??= options.symmetricEncryptionKey;
397
+ signatureSecret ??= options.symemeticSignatureSecret;
398
+ return Buffer.from(
399
+ symmetricDecrypt(encryptedKey, {
400
+ encryptionKey,
401
+ signatureSecret,
402
+ sub,
403
+ encoding: options.symmetricEncryptionEncoding,
404
+ }),
405
+ options.symmetricEncryptionEncoding,
406
+ );
407
+ };
408
+
409
+ export const symmetricDecrypt = (
410
+ encryptedDataPacket,
411
+ { encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding },
328
412
  ) => {
329
- const iv = Buffer.from(data.substring(0, 24), decoding)
330
- const authTag = Buffer.from(data.substring(24, 56), decoding)
331
- const encryptedData = Buffer.from(data.substring(56), decoding)
413
+ if (encryptedKey) {
414
+ encryptionKey ??= symmetricDecryptKey(encryptedKey, {
415
+ signatureSecret,
416
+ sub,
417
+ });
418
+ }
419
+ if (!encryptionKey || !encryptedDataPacket) return encryptedDataPacket;
420
+ decoding ??= options.symmetricEncryptionEncoding;
421
+ encoding ??= "utf8";
422
+
423
+ // remove signature when successful
424
+ encryptedDataPacket = symmetricSignatureVerify(encryptedDataPacket, {
425
+ signatureSecret,
426
+ });
427
+
428
+ if (encryptedDataPacket === false) {
429
+ throw new Error("Signature incorrect");
430
+ }
431
+ const iv = Buffer.from(
432
+ encryptedDataPacket.substring(0, symmetricEncryptionEncodingLengths.iv),
433
+ decoding,
434
+ );
435
+ const authTag = Buffer.from(
436
+ encryptedDataPacket.substring(
437
+ symmetricEncryptionEncodingLengths.iv,
438
+ symmetricEncryptionEncodingLengths.ivAndAuthTag,
439
+ ),
440
+ decoding,
441
+ );
442
+ const encryptedData = Buffer.from(
443
+ encryptedDataPacket.substring(
444
+ symmetricEncryptionEncodingLengths.ivAndAuthTag,
445
+ ),
446
+ decoding,
447
+ );
332
448
 
333
449
  const decipher = createDecipheriv(
334
- options.encryptionMethod,
450
+ options.symmetricEncryptionAlgorithm,
335
451
  encryptionKey,
336
452
  iv,
337
453
  {
338
- authTagLength
339
- }
340
- )
341
- decipher.setAAD(Buffer.from(assocData, 'utf8'))
342
- decipher.setAuthTag(authTag)
343
- return (
454
+ authTagLength,
455
+ },
456
+ );
457
+ decipher.setAAD(sub);
458
+
459
+ decipher.setAuthTag(authTag);
460
+ const data =
344
461
  decipher.update(encryptedData, decoding, encoding) +
345
- decipher.final(encoding)
346
- )
347
- }
348
-
349
- // *** Signatures *** //
350
- export const makeAsymmetricKeys = async (encryptionKey) => {
351
- const { publicKey, privateKey } = await generateKeyPair('ec', {
352
- namedCurve: 'P-384', // P-512
353
- paramEncoding: 'named',
462
+ decipher.final(encoding);
463
+ return data;
464
+ };
465
+
466
+ // *** Symmetric Signatures *** //
467
+ export const symmetricRandomSignatureSecret = () => {
468
+ return randomBytes(32); // 256 bits
469
+ };
470
+
471
+ export const symmetricGenerateSignatureSecret = () => {
472
+ const signatureSecret = symmetricRandomSignatureSecret();
473
+ return { signatureSecret };
474
+ };
475
+
476
+ export const symmetricSignatureSign = (
477
+ data,
478
+ { hashAlgorithm, signatureSecret } = {},
479
+ ) => {
480
+ signatureSecret ??= options.symmetricSignatureSecret;
481
+ hashAlgorithm ??= options.symmetricSignatureHashAlgorithm;
482
+ const signature = createHmac(hashAlgorithm, signatureSecret)
483
+ .update(data)
484
+ .digest(options.symmetricSignatureEncoding)
485
+ .replace(/=+$/, "");
486
+
487
+ const signedData = data + "." + signature;
488
+ return signedData;
489
+ };
490
+
491
+ export const symmetricSignatureVerify = (
492
+ signedData,
493
+ { hashAlgorithm, signatureSecret } = {},
494
+ ) => {
495
+ if (typeof signedData !== "string") return false;
496
+ let lastIndexOf = signedData.lastIndexOf(".");
497
+ // Test for unsigned
498
+ if (lastIndexOf < 0) {
499
+ lastIndexOf = signedData.length;
500
+ }
501
+ const data = signedData.substring(0, lastIndexOf);
502
+ const signedDataExpected = symmetricSignatureSign(data, {
503
+ hashAlgorithm,
504
+ signatureSecret,
505
+ });
506
+ return safeEqual(signedData, signedDataExpected) && data;
507
+ };
508
+
509
+ // Allow rotation of global encryption key, global signature secret, and row encryption key
510
+ export const symmetricRotation = (
511
+ oldEncryptedValues,
512
+ oldOptions, // { encryptionKey, signatureSecret, sub, decoding, encoding }, // old
513
+ oldFields = [],
514
+ newOptions, // { encryptionKey, signatureSecret, sub, decoding, encoding }, // new
515
+ newFields,
516
+ transform = (data) => {
517
+ return data;
518
+ },
519
+ ) => {
520
+ newOptions ??= oldOptions;
521
+ newFields ??= oldFields;
522
+
523
+ if (oldOptions.sub !== newOptions.sub) throw new Error("Mismatching `sub`");
524
+
525
+ oldEncryptedValues = structuredClone(oldEncryptedValues);
526
+ // Don't use structuredClone, converts Buffer to Uint8Array ...
527
+ oldOptions = { ...oldOptions };
528
+ newOptions = { ...newOptions };
529
+
530
+ // decrypt old encryption key
531
+ const { encryptionKey: oldEncryptedKey } = oldEncryptedValues;
532
+ delete oldEncryptedValues.encryptionKey;
533
+
534
+ const oldEncryptionKey = symmetricDecryptKey(oldEncryptedKey, oldOptions);
535
+ oldOptions.encryptionKey = oldEncryptionKey;
536
+
537
+ // decrypt
538
+ const data = transform(
539
+ symmetricDecryptFields(
540
+ oldEncryptedValues,
541
+ { ...oldOptions, encryptionKey: oldEncryptionKey },
542
+ oldFields,
543
+ ),
544
+ );
545
+
546
+ // rotate encryptionKey
547
+ const { encryptionKey: newEncryptionKey, encryptedKey: newEncryptedKey } =
548
+ symmetricGenerateEncryptionKey(newOptions.sub, newOptions);
549
+ newOptions.encryptionKey = newEncryptionKey;
550
+
551
+ // encrypt
552
+ const newEncryptedValues = symmetricEncryptFields(
553
+ data,
554
+ newOptions,
555
+ newFields,
556
+ );
557
+ newEncryptedValues.encryptionKey = newEncryptedKey;
558
+
559
+ return newEncryptedValues;
560
+ };
561
+
562
+ // *** Asymmetric Signatures *** //
563
+ // asymmetricKeyPairType
564
+ export const makeAsymmetricKeys = async () => {
565
+ const { publicKey, privateKey } = await generateKeyPair("ec", {
566
+ namedCurve: options.asymmetricKeyNamedCurve,
567
+ paramEncoding: "named",
354
568
  publicKeyEncoding: {
355
- type: 'spki',
356
- format: 'pem'
569
+ type: "spki",
570
+ format: "pem",
357
571
  },
358
572
  privateKeyEncoding: {
359
- type: 'sec1',
360
- format: 'pem',
361
- // TODO remove encryption for other with sub check?
362
- cipher: options.encryptionMethod,
363
- passphrase: encryptionKey
364
- }
365
- })
366
- return { publicKey, privateKey }
367
- }
368
-
369
- export const makeSignature = (data, privateKey, algorithm = 'SHA3-256') => {
370
- return sign(algorithm, Buffer.from(data), privateKey).toString('base64')
371
- }
372
- export const verifySignature = (
573
+ type: "sec1",
574
+ format: "pem",
575
+ // Encryption done at another level for consistency
576
+ // cipher: options.asymmetricEncryptionAlgorithm,
577
+ // passphrase: encryptionKey,
578
+ },
579
+ });
580
+ return { publicKey, privateKey };
581
+ };
582
+
583
+ export const makeAsymmetricSignature = async (
584
+ data,
585
+ privateKey,
586
+ { hashAlgorithm } = {},
587
+ ) => {
588
+ hashAlgorithm ??= options.asymmetricSignatureHashAlgorithm;
589
+ return (await sign(hashAlgorithm, Buffer.from(data), privateKey)).toString(
590
+ options.asymmetricSignatureEncoding,
591
+ );
592
+ };
593
+ export const verifyAsymmetricSignature = async (
373
594
  data,
374
595
  publicKey,
375
596
  signature,
376
- algorithm = 'SHA3-256'
597
+ { hashAlgorithm } = {},
377
598
  ) => {
378
- return verify(
379
- algorithm,
599
+ hashAlgorithm ??= options.asymmetricSignatureHashAlgorithm;
600
+ return await verify(
601
+ hashAlgorithm,
380
602
  Buffer.from(data),
381
603
  publicKey,
382
- Buffer.from(signature, 'base64')
383
- )
384
- }
385
- // const assocData = 'sub'
386
- // const input = 'data'
387
- //
388
- // const {encryptedKey} = makeSymetricKey(assocData)
389
- // const encryptedData = encrypt(input, encryptedKey, assocData)
390
- // const output = decrypt(encryptedData, encryptedKey, assocData)
391
- // console.log(assocData, input, output, encryptedData)
392
-
393
- // const assocData = 'tlSIcVp1XEF'
394
- // const encryptionKey =
395
- // 'b9ebfdab6603dd8e151138ec8298aaf8b49bf6d38480f9864e9e72e65c4d8b59.b9f9bc89d7f3ce4aa1e1af469d10508b.aa1b6a682fcc186e867a9cfd'
396
- // const value =
397
- // '76ebdea75ea529c16f1645d5435413934cbb97378293776cb446331e6d8323865a808b7f0822cd4f36ef6cca57354ea7c955c94d232a55038321184f4294971315fda4b8dc2f33422bbd3a5c978edc0f5bf019f818f0c92f7c570bdc49fe2f71df3856655198b6723fa52697b9225b2e79f04ed80cd35ef61a8e1e06c3751d3f9caa40aa9a41528ce5ab1bfae8ecff0feab784497ccbf2c7dca4461c6b9a74d1ded6233dc38ad75d0fe2f57cfda2bd1d59ed1f8c53de911cf8071dc7cefde7870c303137586bc0c5f85fe93505d9056994a4d8e59df2027b564c303aa723e98ae5934b15039f90d21827ac22b824593f334466622582a2857f04d8bf450310a9924060d113d2ece0259da68376b31a3c2e8a0b79e81f8efe96bf2b7a6b5b1841edca2566a0a89ab80ee6d6ac2ed24e788ad318a1238dfa5b9e965d256935d3aeafe52f986a530b2f9225bbe7a73b43e6515ce789701f501546239460bccc46285f3fcff0c54c2b6df820334a974abea81460fd4ac99a13cb88e296a2275f80f4c783757319417078458977461dd5e101f9f9b7d303578379dc15ee1b549e3fcceffffba895ecc8291d1f82e150ce8e10640fd79f2e0666167ef7d529cd28cb76a72bf2b3e239dbb845bedd39f42c938bbe711542231fb835edb547d919b3c695272f4cd43640eb3bed2b86948278e5aeeb67ef932ef08332730e75752fd5bd247e2dd30470fc493362d2ecac72237dfbf0a1694f8e6b8c18ab24c71eb723a3f610a056a73fea90f4f8660bfc6dec46fa52a55a3d72a5c9b97770bff7d52ea91b97cc643d915341642e201918fcb088986e9134e133519c6575048e3f840622e569fc5fa66e4f0c31681c9de4b1b1f6acb179728bc0a9f9950bf2e0c8d49da835298013977a2f97d7052bcbedbc39b875bbf9a0622ede589810175ab2455ea72a8274659af7066a2c97ba3aacce84531d8685d08b15d3619038997ae459ac916a88bedaf619f574236d1b86b02f29edaf78684c91c6ee45199b861359ee374fecb95be8a85dc012991d22564adeb2ec95f160574858fa010c40ce74077a08f0e681eb4b10a0093e885f115a12469a82b07012ad4e20b836d5711358c47969af89d09c9299edc540321fe897312ca49c9e8f93d50dc68394d0cc1d108f38e3534821b979c405c1f526a3b57651897873aeeb0909b176fb2034f9fdb1d43a8003809af29ff0bdc456a593029c4526b270908ac94b620a7cb8b7da20a6421e0f2671c899c20d78e1e0a1c3ab0b92948c90fca3c36d21a5e53143bc7e061773251364f8ce3b8992b6313c9604dc19c018b9efc6ca8f7c1fe039077fd76775461d1fa3968f51be7e340e3f09b40d6b44c77a7cbf7d71cad2ddea1121b31d27470a38cda8f8d38170a0d8f0b02d56f8e97b2134597d6111ea6670545bd4f7b4e4d6d7cf2fb5b787a68ba8ee84775f06db195f47282d9646dfbdbb57701e2ef5a432b621cb54a70e94de6b7a3f95bfe3c50793e08a27f76f832070fb2d23ed19dcd75f798aeff0c3e76d8215d43e1a8b7e643bffa243691adf2abcd52f465390e92e0344e2f452673f12d50e2ead0496ba76504ae0f78c81f2a2b5a64b39d3fb35f39f178b9a1587d1f07ed28e7696bfc8740f429f6cc35c964ae04ef39c1070eab6415be3aab887ff6192ac4d49ba9b169221b47d73c55f6010dda07a77cbcf54b779d380591786ba70c6d7000af57034539487f801aeb5c097939682e1530bc26c0b030bcab327ddaaaac5c9b969301b6d4ed7f601a46e8431e8cae3800c80c42686fee3b7625742798ced5976d163d2c8aad1bdac36c001e4e246fdb7b4c6850ed99554cfc0c07b7c7acc7659fd042989cc291ec2bd03a59e439860a5c48e430780f9d85c9acb3e85e741e73163f29afb7c43f143e7ada99a4670cbbe2210bfce8d36ee86.a5237f2d798c9f98b26a676794d3c3f6.0f15d0f53657150aeb1462d5'
398
- // const output = decrypt(value, encryptionKey, assocData)
399
- // console.log(output)
400
- // *** TOTP *** //
604
+ Buffer.from(signature, options.asymmetricSignatureEncoding),
605
+ );
606
+ };
607
+
608
+ export const safeEqual = (input, expected) => {
609
+ const bufferInput = Buffer.from(input);
610
+ const bufferExpected = Buffer.from(expected);
611
+ return (
612
+ bufferInput.length === bufferExpected.length &&
613
+ timingSafeEqual(bufferInput, bufferExpected)
614
+ );
615
+ };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@1auth/crypto",
3
- "version": "0.0.0-alpha.7",
3
+ "version": "0.0.0-alpha.71",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "engines": {
7
- "node": ">=16"
7
+ "node": ">=20"
8
8
  },
9
9
  "engineStrict": true,
10
10
  "publishConfig": {
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "scripts": {
29
29
  "test": "npm run test:unit",
30
- "test:unit": "ava"
30
+ "test:unit": "node --test"
31
31
  },
32
32
  "license": "MIT",
33
33
  "funding": {
@@ -41,15 +41,16 @@
41
41
  },
42
42
  "repository": {
43
43
  "type": "git",
44
- "url": "github:willfarrell/1auth",
44
+ "url": "git+https://github.com/willfarrell/1auth.git",
45
45
  "directory": "packages/crypto"
46
46
  },
47
47
  "bugs": {
48
48
  "url": "https://github.com/willfarrell/1auth/issues"
49
49
  },
50
50
  "homepage": "https://github.com/willfarrell/1auth",
51
- "gitHead": "39c704fcab5798f6bee8d00b2bcf574ba250a4cf",
51
+ "gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
52
52
  "dependencies": {
53
- "@node-rs/argon2": "1.5.0"
53
+ "@node-rs/argon2": "2.0.2",
54
+ "nanoid": "5.1.5"
54
55
  }
55
56
  }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2023 will Farrell
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.