@1auth/crypto 0.0.0-alpha.6 → 0.0.0-alpha.60

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