-
-
Notifications
You must be signed in to change notification settings - Fork 310
/
decrypt.js
235 lines (199 loc) · 8.15 KB
/
decrypt.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
const { inflateRawSync } = require('zlib')
const base64url = require('../help/base64url')
const getKey = require('../help/get_key')
const { KeyStore } = require('../jwks')
const errors = require('../errors')
const { check, decrypt, keyManagementDecrypt } = require('../jwa')
const JWK = require('../jwk')
const { createSecretKey } = require('../help/key_object')
const generateCEK = require('./generate_cek')
const validateHeaders = require('./validate_headers')
const { detect: resolveSerialization } = require('./serializers')
const SINGLE_RECIPIENT = new Set(['compact', 'flattened'])
const combineHeader = (prot = {}, unprotected = {}, header = {}) => {
if (typeof prot === 'string') {
prot = base64url.JSON.decode(prot)
}
const p2s = prot.p2s || unprotected.p2s || header.p2s
const apu = prot.apu || unprotected.apu || header.apu
const apv = prot.apv || unprotected.apv || header.apv
const iv = prot.iv || unprotected.iv || header.iv
const tag = prot.tag || unprotected.tag || header.tag
return {
...prot,
...unprotected,
...header,
...(typeof p2s === 'string' ? { p2s: base64url.decodeToBuffer(p2s) } : undefined),
...(typeof apu === 'string' ? { apu: base64url.decodeToBuffer(apu) } : undefined),
...(typeof apv === 'string' ? { apv: base64url.decodeToBuffer(apv) } : undefined),
...(typeof iv === 'string' ? { iv: base64url.decodeToBuffer(iv) } : undefined),
...(typeof tag === 'string' ? { tag: base64url.decodeToBuffer(tag) } : undefined)
}
}
const validateAlgorithms = (algorithms, option) => {
if (algorithms !== undefined && (!Array.isArray(algorithms) || algorithms.some(s => typeof s !== 'string' || !s))) {
throw new TypeError(`"${option}" option must be an array of non-empty strings`)
}
if (!algorithms) {
return undefined
}
return new Set(algorithms)
}
/*
* @public
*/
const jweDecrypt = (skipValidateHeaders, serialization, jwe, key, { crit = [], complete = false, keyManagementAlgorithms, contentEncryptionAlgorithms, maxPBES2Count = 10000, inflateRawSyncLimit = 250000 } = {}) => {
key = getKey(key, true)
keyManagementAlgorithms = validateAlgorithms(keyManagementAlgorithms, 'keyManagementAlgorithms')
contentEncryptionAlgorithms = validateAlgorithms(contentEncryptionAlgorithms, 'contentEncryptionAlgorithms')
if (!Array.isArray(crit) || crit.some(s => typeof s !== 'string' || !s)) {
throw new TypeError('"crit" option must be an array of non-empty strings')
}
if (!serialization) {
serialization = resolveSerialization(jwe)
}
let alg, ciphertext, enc, encryptedKey, iv, opts, prot, tag, unprotected, cek, aad, header
// treat general format with one recipient as flattened
// skips iteration and avoids multi errors in this case
if (serialization === 'general' && jwe.recipients.length === 1) {
serialization = 'flattened'
const { recipients, ...root } = jwe
jwe = { ...root, ...recipients[0] }
}
if (SINGLE_RECIPIENT.has(serialization)) {
if (serialization === 'compact') { // compact serialization format
([prot, encryptedKey, iv, ciphertext, tag] = jwe.split('.'))
} else { // flattened serialization format
({ protected: prot, encrypted_key: encryptedKey, iv, ciphertext, tag, unprotected, aad, header } = jwe)
}
if (!skipValidateHeaders) {
validateHeaders(prot, unprotected, [{ header }], true, crit)
}
opts = combineHeader(prot, unprotected, header)
;({ alg, enc } = opts)
if (keyManagementAlgorithms && !keyManagementAlgorithms.has(alg)) {
throw new errors.JOSEAlgNotWhitelisted('key management algorithm not whitelisted')
}
if (contentEncryptionAlgorithms && !contentEncryptionAlgorithms.has(enc)) {
throw new errors.JOSEAlgNotWhitelisted('content encryption algorithm not whitelisted')
}
if (key instanceof KeyStore) {
const keystore = key
let keys
if (opts.alg === 'dir') {
keys = keystore.all({ kid: opts.kid, alg: opts.enc, key_ops: ['decrypt'] })
} else {
keys = keystore.all({ kid: opts.kid, alg: opts.alg, key_ops: ['unwrapKey'] })
}
switch (keys.length) {
case 0:
throw new errors.JWKSNoMatchingKey()
case 1:
// treat the call as if a Key instance was passed in
// skips iteration and avoids multi errors in this case
key = keys[0]
break
default: {
const errs = []
for (const key of keys) {
try {
return jweDecrypt(true, serialization, jwe, key, {
crit,
complete,
contentEncryptionAlgorithms: contentEncryptionAlgorithms ? [...contentEncryptionAlgorithms] : undefined,
keyManagementAlgorithms: keyManagementAlgorithms ? [...keyManagementAlgorithms] : undefined
})
} catch (err) {
errs.push(err)
continue
}
}
const multi = new errors.JOSEMultiError(errs)
if ([...multi].some(e => e instanceof errors.JWEDecryptionFailed)) {
throw new errors.JWEDecryptionFailed()
}
throw multi
}
}
}
check(key, ...(alg === 'dir' ? ['decrypt', enc] : ['keyManagementDecrypt', alg]))
if (alg.startsWith('PBES2')) {
if (opts && opts.p2c > maxPBES2Count) {
throw new errors.JWEInvalid('JOSE Header "p2c" (PBES2 Count) out is of acceptable bounds')
}
}
try {
if (alg === 'dir') {
cek = JWK.asKey(key, { alg: enc, use: 'enc' })
} else if (alg === 'ECDH-ES') {
const unwrapped = keyManagementDecrypt(alg, key, undefined, opts)
cek = JWK.asKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
} else {
const unwrapped = keyManagementDecrypt(alg, key, base64url.decodeToBuffer(encryptedKey), opts)
cek = JWK.asKey(createSecretKey(unwrapped), { alg: enc, use: 'enc' })
}
} catch (err) {
// To mitigate the attacks described in RFC 3218, the
// recipient MUST NOT distinguish between format, padding, and length
// errors of encrypted keys. It is strongly recommended, in the event
// of receiving an improperly formatted key, that the recipient
// substitute a randomly generated CEK and proceed to the next step, to
// mitigate timing attacks.
cek = generateCEK(enc)
}
let adata
if (aad) {
adata = Buffer.concat([
Buffer.from(prot || ''),
Buffer.from('.'),
Buffer.from(aad)
])
} else {
adata = Buffer.from(prot || '')
}
try {
iv = base64url.decodeToBuffer(iv)
} catch (err) {}
try {
tag = base64url.decodeToBuffer(tag)
} catch (err) {}
let cleartext = decrypt(enc, cek, base64url.decodeToBuffer(ciphertext), { iv, tag, aad: adata })
if (opts.zip) {
cleartext = inflateRawSync(cleartext, { maxOutputLength: inflateRawSyncLimit })
}
if (complete) {
const result = { cleartext, key, cek }
if (aad) result.aad = aad
if (header) result.header = header
if (unprotected) result.unprotected = unprotected
if (prot) result.protected = base64url.JSON.decode(prot)
return result
}
return cleartext
}
validateHeaders(jwe.protected, jwe.unprotected, jwe.recipients.map(({ header }) => ({ header })), true, crit)
// general serialization format
const { recipients, ...root } = jwe
const errs = []
for (const recipient of recipients) {
try {
return jweDecrypt(true, 'flattened', { ...root, ...recipient }, key, {
crit,
complete,
contentEncryptionAlgorithms: contentEncryptionAlgorithms ? [...contentEncryptionAlgorithms] : undefined,
keyManagementAlgorithms: keyManagementAlgorithms ? [...keyManagementAlgorithms] : undefined
})
} catch (err) {
errs.push(err)
continue
}
}
const multi = new errors.JOSEMultiError(errs)
if ([...multi].some(e => e instanceof errors.JWEDecryptionFailed)) {
throw new errors.JWEDecryptionFailed()
} else if ([...multi].every(e => e instanceof errors.JWKSNoMatchingKey)) {
throw new errors.JWKSNoMatchingKey()
}
throw multi
}
module.exports = jweDecrypt.bind(undefined, false, undefined)