1 | const { inflateRawSync } = require('zlib')
|
2 |
|
3 | const base64url = require('../help/base64url')
|
4 | const getKey = require('../help/get_key')
|
5 | const { KeyStore } = require('../jwks')
|
6 | const errors = require('../errors')
|
7 | const { check, decrypt, keyManagementDecrypt } = require('../jwa')
|
8 | const JWK = require('../jwk')
|
9 |
|
10 | const { createSecretKey } = require('../help/key_object')
|
11 | const generateCEK = require('./generate_cek')
|
12 | const validateHeaders = require('./validate_headers')
|
13 | const { detect: resolveSerialization } = require('./serializers')
|
14 |
|
15 | const SINGLE_RECIPIENT = new Set(['compact', 'flattened'])
|
16 |
|
17 | const 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 |
|
42 |
|
43 | const 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 |
|
63 |
|
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') {
|
72 | ([prot, encryptedKey, iv, ciphertext, tag] = jwe.split('.'))
|
73 | } else {
|
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 |
|
102 |
|
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 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
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 |
|
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 |
|
206 | module.exports = jweDecrypt.bind(undefined, false, undefined)
|