@1auth/crypto 0.0.0-alpha.5 → 0.0.0-alpha.51
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 +368 -274
- package/package.json +5 -5
- package/LICENSE +0 -21
package/index.js
CHANGED
|
@@ -4,27 +4,56 @@ import {
|
|
|
4
4
|
createHash as checksum,
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const generateKeyPair = promisify(generateKeyPairCallback)
|
|
17
|
+
const sign = promisify(signCallback)
|
|
18
|
+
const verify = promisify(verifyCallback)
|
|
19
|
+
|
|
20
|
+
const defaults = {
|
|
21
|
+
// symmetricEncryptionKey: randomBytes(32).toString('base64') // 256 bits
|
|
22
|
+
symmetricEncryptionKey: undefined,
|
|
23
|
+
symmetricEncryptionMethod: 'chacha20-poly1305', // 2024-05: AES-256 GCM (aes-256-gcm) or ChaCha20-Poly1305 (chacha20-poly1305)
|
|
24
|
+
symmetricEncryptionEncoding: 'base64', // https://nodejs.org/api/buffer.html#buffers-and-character-encodings
|
|
25
|
+
symmetricSignatureAlgorithm: 'sha3-384',
|
|
26
|
+
// symmetricSignatureSecret: randomBytes(32).toString('base64') // 256 bits
|
|
27
|
+
symmetricSignatureSecret: undefined,
|
|
28
|
+
asymmetricKeyNamedCurve: 'P-384', // P-512
|
|
29
|
+
asymmetricSignatureAlgorithm: 'sha3-384',
|
|
30
|
+
digestAlgorithm: 'sha3-384',
|
|
31
|
+
digestEncoding: 'hex',
|
|
32
|
+
signatureEncoding: 'base64'
|
|
21
33
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const symmetricEncryptionEncodingLengths = {}
|
|
35
|
+
const options = {}
|
|
36
|
+
export default (opt = {}) => {
|
|
37
|
+
Object.assign(options, defaults, opt)
|
|
38
|
+
symmetricEncryptionEncodingLengths.iv = randomBytes(12).toString(
|
|
39
|
+
options.symmetricEncryptionEncoding
|
|
40
|
+
).length
|
|
41
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag =
|
|
42
|
+
symmetricEncryptionEncodingLengths.iv +
|
|
43
|
+
randomBytes(16).toString(options.symmetricEncryptionEncoding).length
|
|
44
|
+
if (!options.symmetricEncryptionKey) {
|
|
45
|
+
console.warn(
|
|
46
|
+
"@1auth/crypto symmetricEncryptionKey is empty, use a stored secret made from randomBytes(32).toString('base64'). Encryption disabled."
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
if (!options.symmetricSignatureSecret) {
|
|
50
|
+
console.warn(
|
|
51
|
+
"@1auth/crypto symmetricSignatureSecret is empty, use a stored secret made from randomBytes(32).toString('base64'). Signature disabled."
|
|
52
|
+
)
|
|
53
|
+
}
|
|
25
54
|
}
|
|
26
55
|
|
|
27
|
-
const
|
|
56
|
+
export const getOptions = () => options
|
|
28
57
|
|
|
29
58
|
// *** entropy *** //
|
|
30
59
|
// export const characterPoolSize = (value) => {
|
|
@@ -42,184 +71,132 @@ const generateKeyPair = promisify(generateKeyPairCallback)
|
|
|
42
71
|
// return max - min
|
|
43
72
|
// }
|
|
44
73
|
|
|
45
|
-
// ***
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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('=', '')
|
|
74
|
+
// *** Helpers *** //
|
|
75
|
+
// Ref: https://therootcompany.com/blog/how-many-bits-of-entropy-per-character/
|
|
76
|
+
export const entropyToCharacterLength = (bits, characterPoolSize) => {
|
|
77
|
+
// bits*ln(2)/ln(characterPoolSize)
|
|
78
|
+
return Math.ceil((bits * Math.LN2) / Math.log(characterPoolSize))
|
|
73
79
|
}
|
|
80
|
+
/* export const characterLengthToEntropy = (
|
|
81
|
+
characterLength,
|
|
82
|
+
characterPoolSize,
|
|
83
|
+
) => {
|
|
84
|
+
// log_2(characterPoolSize^characterLength)
|
|
85
|
+
return Math.floor(Math.log2(characterPoolSize ** characterLength));
|
|
86
|
+
}; */
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
// *** Random generators *** //
|
|
89
|
+
// Ref: https://github.com/sindresorhus/crypto-random-string/blob/main/core.js
|
|
90
|
+
export const charactersAlphaNumeric = [
|
|
91
|
+
...'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
92
|
+
]
|
|
93
|
+
export const charactersDistinguishable = [...'CDEHKMPRTUWXY012458']
|
|
94
|
+
export const charactersNumeric = [...'0123456789']
|
|
95
|
+
|
|
96
|
+
export const randomCharacters = (
|
|
97
|
+
length,
|
|
98
|
+
characters = charactersAlphaNumeric
|
|
99
|
+
) => {
|
|
100
|
+
// Generating entropy is faster than complex math operations, so we use the simplest way
|
|
101
|
+
const characterCount = characters.length
|
|
102
|
+
const maxValidSelector =
|
|
103
|
+
Math.floor(0x1_00_00 / characterCount) * characterCount - 1 // Using values above this will ruin distribution when using modular division
|
|
104
|
+
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
|
|
105
|
+
let string = ''
|
|
106
|
+
let stringLength = 0
|
|
107
|
+
|
|
108
|
+
while (stringLength < length) {
|
|
109
|
+
// In case we had many bad values, which may happen for character sets of size above 0x8000 but close to it
|
|
110
|
+
const entropy = new Uint8Array(randomBytes(entropyLength))
|
|
111
|
+
let entropyPosition = 0
|
|
112
|
+
|
|
113
|
+
while (entropyPosition < entropyLength && stringLength < length) {
|
|
114
|
+
const entropyValue =
|
|
115
|
+
entropy[entropyPosition] + (entropy[entropyPosition + 1] << 8) // eslint-disable-line no-bitwise
|
|
116
|
+
entropyPosition += 2
|
|
117
|
+
if (entropyValue > maxValidSelector) {
|
|
118
|
+
// Skip values which will ruin distribution when using modular division
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
string += characters[entropyValue % characterCount]
|
|
123
|
+
stringLength++
|
|
124
|
+
}
|
|
84
125
|
}
|
|
85
|
-
while (value.length < characterLength) value = '0' + value
|
|
86
|
-
return value
|
|
87
|
-
}
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
const characterLength = entropyToCharacterLength(bits, characterPoolSize.hex)
|
|
91
|
-
return randomBytes(characterLength).toString('hex')
|
|
127
|
+
return string
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
type: 'id',
|
|
97
|
-
entropy: 64,
|
|
98
|
-
charPool: characterPoolSize.alphaNumeric,
|
|
99
|
-
create: async () => randomAlphaNumeric(randomId.entropy)
|
|
130
|
+
export const randomAlphaNumeric = (characterLength) => {
|
|
131
|
+
return randomCharacters(characterLength, charactersAlphaNumeric)
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
export const
|
|
103
|
-
|
|
104
|
-
entropy: 64,
|
|
105
|
-
charPool: characterPoolSize.alphaNumeric,
|
|
106
|
-
create: async () => randomAlphaNumeric(subject.entropy)
|
|
134
|
+
export const randomNumeric = (characterLength) => {
|
|
135
|
+
return randomCharacters(characterLength, charactersNumeric)
|
|
107
136
|
}
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
// *** configs *** //
|
|
139
|
+
export const randomId = {
|
|
110
140
|
type: 'id',
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
141
|
+
minLength: entropyToCharacterLength(64, charactersAlphaNumeric.length),
|
|
142
|
+
// TODO update to use https://github.com/jetpack-io/typeid
|
|
143
|
+
create: (prefix) =>
|
|
144
|
+
(prefix ? prefix + '_' : '') + randomAlphaNumeric(randomId.minLength)
|
|
160
145
|
}
|
|
161
146
|
|
|
162
|
-
//
|
|
163
|
-
export const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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)
|
|
147
|
+
// *** Digests *** //
|
|
148
|
+
export const createChecksum = (value, { algorithm, encoding } = {}) => {
|
|
149
|
+
algorithm ??= options.digestAlgorithm
|
|
150
|
+
encoding ??= options.digestEncoding
|
|
151
|
+
return checksum(algorithm).update(value).digest(encoding)
|
|
173
152
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
153
|
+
export const createDigest = (value, { algorithm } = {}) => {
|
|
154
|
+
algorithm ??= options.digestAlgorithm
|
|
155
|
+
const checksum = createChecksum(value, { algorithm })
|
|
156
|
+
return `${algorithm}:${checksum}`
|
|
186
157
|
}
|
|
187
158
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
159
|
+
// TODO evaluate non-iv encryption approach?
|
|
160
|
+
// Use encryption as pepper to allow easier rotation of pepper
|
|
161
|
+
export const createEncryptedDigest = (value, { algorithm } = {}) => {
|
|
162
|
+
algorithm ??= options.digestAlgorithm
|
|
163
|
+
const digest = createDigest(value, { algorithm })
|
|
164
|
+
// encrypting using the symmetricEncryptionKey instead of
|
|
165
|
+
// the row encryptionKey to allow lookup, must have fixed iv
|
|
166
|
+
return symmetricEncrypt(digest, {
|
|
167
|
+
encryptionKey: options.symmetricEncryptionKey,
|
|
168
|
+
sub: '',
|
|
169
|
+
encoding: options.symmetricEncryptionEncoding,
|
|
170
|
+
iv: Buffer.from(
|
|
171
|
+
options.symmetricEncryptionKey.substring(
|
|
172
|
+
0,
|
|
173
|
+
symmetricEncryptionEncodingLengths.iv
|
|
174
|
+
),
|
|
175
|
+
options.symmetricEncryptionEncoding
|
|
176
|
+
)
|
|
177
|
+
})
|
|
199
178
|
}
|
|
200
179
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
180
|
+
export const rotateDigestEncryption = (
|
|
181
|
+
encryptedValue,
|
|
182
|
+
encryptionKey,
|
|
183
|
+
newEncryptionKey
|
|
205
184
|
) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// bits*ln(2)/ln(characterPoolSize)
|
|
212
|
-
return Math.round((bits * Math.LN2) / Math.log(characterPoolSize))
|
|
213
|
-
}
|
|
185
|
+
const digest = symmetricDecrypt(encryptedValue, {
|
|
186
|
+
encryptionKey,
|
|
187
|
+
sub: '',
|
|
188
|
+
encoding: options.symmetricEncryptionEncoding
|
|
189
|
+
})
|
|
214
190
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
191
|
+
return symmetricEncrypt(digest, {
|
|
192
|
+
encryptionKey: newEncryptionKey,
|
|
193
|
+
sub: '',
|
|
194
|
+
encoding: options.symmetricEncryptionEncoding,
|
|
195
|
+
iv: Buffer.from(
|
|
196
|
+
newEncryptionKey.substring(0, symmetricEncryptionEncodingLengths.iv),
|
|
197
|
+
options.symmetricEncryptionEncoding
|
|
198
|
+
)
|
|
199
|
+
})
|
|
223
200
|
}
|
|
224
201
|
|
|
225
202
|
// *** Hashing *** //
|
|
@@ -241,115 +218,231 @@ export const verifySecretHash = async (hash, value) => {
|
|
|
241
218
|
return secretVerify(hash, value)
|
|
242
219
|
}
|
|
243
220
|
|
|
244
|
-
// *** Encryption *** //
|
|
221
|
+
// *** Symmetric Encryption *** //
|
|
245
222
|
const authTagLength = 16
|
|
246
223
|
|
|
247
|
-
export const
|
|
248
|
-
|
|
224
|
+
export const symmetricRandomEncryptionKey = () => {
|
|
225
|
+
return randomBytes(32).toString('base64') // 256 bits
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const symmetricGenerateEncryptionKey = (sub) => {
|
|
229
|
+
if (!options.symmetricEncryptionKey) {
|
|
249
230
|
return { encryptionKey: '', encryptedKey: '' }
|
|
250
231
|
}
|
|
251
|
-
const encryptionKey =
|
|
252
|
-
const encryptedKey =
|
|
253
|
-
encryptionKey,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
)
|
|
232
|
+
const encryptionKey = symmetricRandomEncryptionKey()
|
|
233
|
+
const encryptedKey = symmetricEncrypt(encryptionKey, {
|
|
234
|
+
encryptionKey: options.symmetricEncryptionKey,
|
|
235
|
+
sub,
|
|
236
|
+
decoding: 'base64',
|
|
237
|
+
encoding: options.symmetricEncryptionEncoding
|
|
238
|
+
})
|
|
259
239
|
return { encryptionKey, encryptedKey }
|
|
260
240
|
}
|
|
261
241
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
242
|
+
// sub add context to encryption
|
|
243
|
+
export const symmetricEncryptFields = (
|
|
244
|
+
values,
|
|
245
|
+
{ encryptedKey, encryptionKey, sub },
|
|
246
|
+
fields = []
|
|
247
|
+
) => {
|
|
248
|
+
if (encryptedKey) {
|
|
249
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
250
|
+
sub
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
if (!encryptionKey) return values
|
|
254
|
+
const encryptedValues = structuredClone(values)
|
|
255
|
+
for (const key of fields) {
|
|
256
|
+
encryptedValues[key] &&= symmetricEncrypt(encryptedValues[key], {
|
|
257
|
+
encryptionKey,
|
|
258
|
+
sub
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
return encryptedValues
|
|
279
262
|
}
|
|
280
263
|
|
|
281
|
-
const
|
|
264
|
+
export const symmetricEncrypt = (
|
|
282
265
|
data,
|
|
283
|
-
encryptionKey,
|
|
284
|
-
assocData,
|
|
285
|
-
decoding = 'utf8',
|
|
286
|
-
encoding = 'hex'
|
|
266
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding, iv }
|
|
287
267
|
) => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
268
|
+
if (encryptedKey) {
|
|
269
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
270
|
+
sub
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
if (!encryptionKey || !data) return data
|
|
274
|
+
decoding ??= 'utf8'
|
|
275
|
+
encoding ??= options.symmetricEncryptionEncoding
|
|
276
|
+
iv ??= randomBytes(12) // 96 bits
|
|
277
|
+
|
|
278
|
+
const cipher = createCipheriv(
|
|
279
|
+
options.symmetricEncryptionMethod,
|
|
280
|
+
Buffer.from(encryptionKey, 'base64'),
|
|
281
|
+
iv,
|
|
282
|
+
{
|
|
283
|
+
authTagLength
|
|
284
|
+
}
|
|
285
|
+
)
|
|
286
|
+
cipher.setAAD(Buffer.from(sub ?? '', 'utf8'))
|
|
293
287
|
const encryptedData =
|
|
294
288
|
cipher.update(data, decoding, encoding) + cipher.final(encoding)
|
|
295
289
|
const authTag = cipher.getAuthTag()
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
290
|
+
|
|
291
|
+
const encryptedDataPacket =
|
|
292
|
+
iv.toString(encoding) + authTag.toString(encoding) + encryptedData
|
|
293
|
+
|
|
294
|
+
// add signature to end
|
|
295
|
+
return symmetricSignatureSign(encryptedDataPacket, signatureSecret)
|
|
301
296
|
}
|
|
302
297
|
|
|
303
|
-
export const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
298
|
+
export const symmetricDecryptFields = (
|
|
299
|
+
encryptedValues,
|
|
300
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub },
|
|
301
|
+
fields = []
|
|
302
|
+
) => {
|
|
303
|
+
if (encryptedKey) {
|
|
304
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
305
|
+
sub
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
if (!encryptionKey) return encryptedValues
|
|
309
|
+
const values = structuredClone(encryptedValues)
|
|
310
|
+
for (const key of fields) {
|
|
311
|
+
values[key] &&= symmetricDecrypt(values[key], {
|
|
312
|
+
encryptionKey,
|
|
313
|
+
sub
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
return values
|
|
320
317
|
}
|
|
321
318
|
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
319
|
+
export const symmetricDecryptKey = (encryptedKey, { sub } = {}) => {
|
|
320
|
+
return symmetricDecrypt(encryptedKey, {
|
|
321
|
+
encryptionKey: options.symmetricEncryptionKey,
|
|
322
|
+
sub,
|
|
323
|
+
decoding: options.symmetricEncryptionEncoding,
|
|
324
|
+
encoding: 'base64'
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const symmetricDecrypt = (
|
|
329
|
+
encryptedDataPacket,
|
|
330
|
+
{ encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding }
|
|
328
331
|
) => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
if (encryptedKey) {
|
|
333
|
+
encryptionKey ??= symmetricDecryptKey(encryptedKey, {
|
|
334
|
+
sub
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
if (!encryptionKey || !encryptedDataPacket) return encryptedDataPacket
|
|
338
|
+
decoding ??= options.symmetricEncryptionEncoding
|
|
339
|
+
encoding ??= 'utf8'
|
|
340
|
+
|
|
341
|
+
// remove signature when successful
|
|
342
|
+
encryptedDataPacket = symmetricSignatureVerify(
|
|
343
|
+
encryptedDataPacket,
|
|
344
|
+
signatureSecret
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
if (encryptedDataPacket === false) {
|
|
348
|
+
throw new Error('Signature incorrect')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const iv = Buffer.from(
|
|
352
|
+
encryptedDataPacket.substring(0, symmetricEncryptionEncodingLengths.iv),
|
|
353
|
+
decoding
|
|
354
|
+
)
|
|
355
|
+
const authTag = Buffer.from(
|
|
356
|
+
encryptedDataPacket.substring(
|
|
357
|
+
symmetricEncryptionEncodingLengths.iv,
|
|
358
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag
|
|
359
|
+
),
|
|
360
|
+
decoding
|
|
361
|
+
)
|
|
362
|
+
const encryptedData = Buffer.from(
|
|
363
|
+
encryptedDataPacket.substring(
|
|
364
|
+
symmetricEncryptionEncodingLengths.ivAndAuthTag
|
|
365
|
+
),
|
|
366
|
+
decoding
|
|
367
|
+
)
|
|
332
368
|
|
|
333
369
|
const decipher = createDecipheriv(
|
|
334
|
-
options.
|
|
335
|
-
encryptionKey,
|
|
370
|
+
options.symmetricEncryptionMethod,
|
|
371
|
+
Buffer.from(encryptionKey, 'base64'),
|
|
336
372
|
iv,
|
|
337
373
|
{
|
|
338
374
|
authTagLength
|
|
339
375
|
}
|
|
340
376
|
)
|
|
341
|
-
decipher.setAAD(Buffer.from(
|
|
377
|
+
decipher.setAAD(Buffer.from(sub ?? '', 'utf8'))
|
|
378
|
+
|
|
342
379
|
decipher.setAuthTag(authTag)
|
|
343
|
-
|
|
380
|
+
const data =
|
|
344
381
|
decipher.update(encryptedData, decoding, encoding) +
|
|
345
382
|
decipher.final(encoding)
|
|
346
|
-
|
|
383
|
+
return data
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// *** Symmetric Signatures *** //
|
|
387
|
+
export const symmetricRandomSignatureSecret = () => {
|
|
388
|
+
return randomBytes(32).toString('base64') // 256 bits
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const symmetricGenerateSignatureSecret = (sub) => {
|
|
392
|
+
if (!options.symmetricSignatureSecret) {
|
|
393
|
+
return { signatureSecret: '' }
|
|
394
|
+
}
|
|
395
|
+
const signatureSecret = symmetricRandomSignatureSecret()
|
|
396
|
+
return { signatureSecret }
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export const symmetricSignatureSign = (
|
|
400
|
+
data,
|
|
401
|
+
signatureSecret,
|
|
402
|
+
{ algorithm } = {}
|
|
403
|
+
) => {
|
|
404
|
+
// if (typeof data !== 'string') throw new TypeError('data must me a string')
|
|
405
|
+
signatureSecret ??= options.symmetricSignatureSecret
|
|
406
|
+
algorithm ??= options.symmetricSignatureAlgorithm
|
|
407
|
+
const signature = createHmac(algorithm, signatureSecret)
|
|
408
|
+
.update(data)
|
|
409
|
+
.digest(options.signatureEncoding)
|
|
410
|
+
.replace(/=+$/, '')
|
|
411
|
+
|
|
412
|
+
const signedData = data + '.' + signature
|
|
413
|
+
return signedData
|
|
347
414
|
}
|
|
348
415
|
|
|
349
|
-
|
|
350
|
-
|
|
416
|
+
export const symmetricSignatureVerify = (
|
|
417
|
+
signedData,
|
|
418
|
+
signatureSecret,
|
|
419
|
+
{ algorithm } = {}
|
|
420
|
+
) => {
|
|
421
|
+
if (typeof signedData !== 'string') return false
|
|
422
|
+
const data = signedData.slice(0, signedData.lastIndexOf('.'))
|
|
423
|
+
const signedDataExpected = symmetricSignatureSign(data, signatureSecret, {
|
|
424
|
+
algorithm
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
return safeEqual(signedData, signedDataExpected) && data
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export const symmetricRotation = (
|
|
431
|
+
encryptedValues,
|
|
432
|
+
oldOptions, // { encryptedKey, encryptionKey, signatureSecret, sub }, // old
|
|
433
|
+
newOptions, // { encryptedKey, encryptionKey, signatureSecret, sub, decoding, encoding, iv }, // new
|
|
434
|
+
fields = []
|
|
435
|
+
) => {
|
|
436
|
+
const data = symmetricDecryptFields(encryptedValues, oldOptions, fields)
|
|
437
|
+
const newEncryptedData = symmetricEncryptFields(data, newOptions, fields)
|
|
438
|
+
return newEncryptedData
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// *** Asymmetric Signatures *** //
|
|
442
|
+
// asymmetricKeyPairType
|
|
443
|
+
export const makeAsymmetricKeys = async () => {
|
|
351
444
|
const { publicKey, privateKey } = await generateKeyPair('ec', {
|
|
352
|
-
namedCurve:
|
|
445
|
+
namedCurve: options.asymmetricKeyNamedCurve,
|
|
353
446
|
paramEncoding: 'named',
|
|
354
447
|
publicKeyEncoding: {
|
|
355
448
|
type: 'spki',
|
|
@@ -357,44 +450,45 @@ export const makeAsymmetricKeys = async (encryptionKey) => {
|
|
|
357
450
|
},
|
|
358
451
|
privateKeyEncoding: {
|
|
359
452
|
type: 'sec1',
|
|
360
|
-
format: 'pem'
|
|
361
|
-
//
|
|
362
|
-
cipher: options.
|
|
363
|
-
passphrase: encryptionKey
|
|
453
|
+
format: 'pem'
|
|
454
|
+
// Encryption done at another level for consistency
|
|
455
|
+
// cipher: options.asymmetricEncryptionMethod,
|
|
456
|
+
// passphrase: encryptionKey,
|
|
364
457
|
}
|
|
365
458
|
})
|
|
366
459
|
return { publicKey, privateKey }
|
|
367
460
|
}
|
|
368
461
|
|
|
369
|
-
export const
|
|
370
|
-
|
|
462
|
+
export const makeAsymmetricSignature = async (
|
|
463
|
+
data,
|
|
464
|
+
privateKey,
|
|
465
|
+
{ algorithm } = {}
|
|
466
|
+
) => {
|
|
467
|
+
algorithm ??= options.asymmetricSignatureAlgorithm
|
|
468
|
+
return (await sign(algorithm, Buffer.from(data), privateKey)).toString(
|
|
469
|
+
options.signatureEncoding
|
|
470
|
+
)
|
|
371
471
|
}
|
|
372
|
-
export const
|
|
472
|
+
export const verifyAsymmetricSignature = async (
|
|
373
473
|
data,
|
|
374
474
|
publicKey,
|
|
375
475
|
signature,
|
|
376
|
-
algorithm =
|
|
476
|
+
{ algorithm } = {}
|
|
377
477
|
) => {
|
|
378
|
-
|
|
478
|
+
algorithm ??= options.asymmetricSignatureAlgorithm
|
|
479
|
+
return await verify(
|
|
379
480
|
algorithm,
|
|
380
481
|
Buffer.from(data),
|
|
381
482
|
publicKey,
|
|
382
|
-
Buffer.from(signature,
|
|
483
|
+
Buffer.from(signature, options.signatureEncoding)
|
|
484
|
+
)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export const safeEqual = (input, expected) => {
|
|
488
|
+
const bufferInput = Buffer.from(input)
|
|
489
|
+
const bufferExpected = Buffer.from(expected)
|
|
490
|
+
return (
|
|
491
|
+
bufferInput.length === bufferExpected.length &&
|
|
492
|
+
timingSafeEqual(bufferInput, bufferExpected)
|
|
383
493
|
)
|
|
384
494
|
}
|
|
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.
|
|
3
|
+
"version": "0.0.0-alpha.51",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
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": "
|
|
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": "
|
|
51
|
+
"gitHead": "7a6c0fbb8ab71d6a2171e678697de9f237568431",
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@node-rs/argon2": "
|
|
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.
|