UNPKG

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