1 | const { deflateRawSync } = require('zlib')
|
2 |
|
3 | const { KEYOBJECT } = require('../help/consts')
|
4 | const generateIV = require('../help/generate_iv')
|
5 | const base64url = require('../help/base64url')
|
6 | const getKey = require('../help/get_key')
|
7 | const isObject = require('../help/is_object')
|
8 | const { createSecretKey } = require('../help/key_object')
|
9 | const deepClone = require('../help/deep_clone')
|
10 | const importKey = require('../jwk/import')
|
11 | const { JWEInvalid } = require('../errors')
|
12 | const { check, keyManagementEncrypt, encrypt } = require('../jwa')
|
13 |
|
14 | const serializers = require('./serializers')
|
15 | const generateCEK = require('./generate_cek')
|
16 | const validateHeaders = require('./validate_headers')
|
17 |
|
18 | const PROCESS_RECIPIENT = Symbol('PROCESS_RECIPIENT')
|
19 |
|
20 | class Encrypt {
|
21 |
|
22 | constructor (cleartext, protectedHeader, unprotectedHeader, aad) {
|
23 | if (!Buffer.isBuffer(cleartext) && typeof cleartext !== 'string') {
|
24 | throw new TypeError('cleartext argument must be a Buffer or a string')
|
25 | }
|
26 | cleartext = Buffer.from(cleartext)
|
27 |
|
28 | if (aad !== undefined && !Buffer.isBuffer(aad) && typeof aad !== 'string') {
|
29 | throw new TypeError('aad argument must be a Buffer or a string when provided')
|
30 | }
|
31 | aad = aad ? Buffer.from(aad) : undefined
|
32 |
|
33 | if (protectedHeader !== undefined && !isObject(protectedHeader)) {
|
34 | throw new TypeError('protectedHeader argument must be a plain object when provided')
|
35 | }
|
36 |
|
37 | if (unprotectedHeader !== undefined && !isObject(unprotectedHeader)) {
|
38 | throw new TypeError('unprotectedHeader argument must be a plain object when provided')
|
39 | }
|
40 |
|
41 | this._recipients = []
|
42 | this._cleartext = cleartext
|
43 | this._aad = aad
|
44 | this._unprotected = unprotectedHeader ? deepClone(unprotectedHeader) : undefined
|
45 | this._protected = protectedHeader ? deepClone(protectedHeader) : undefined
|
46 | }
|
47 |
|
48 | |
49 |
|
50 |
|
51 | recipient (key, header) {
|
52 | key = getKey(key)
|
53 |
|
54 | if (header !== undefined && !isObject(header)) {
|
55 | throw new TypeError('header argument must be a plain object when provided')
|
56 | }
|
57 |
|
58 | this._recipients.push({
|
59 | key,
|
60 | header: header ? deepClone(header) : undefined
|
61 | })
|
62 |
|
63 | return this
|
64 | }
|
65 |
|
66 | |
67 |
|
68 |
|
69 | [PROCESS_RECIPIENT] (recipient) {
|
70 | const unprotectedHeader = this._unprotected
|
71 | const protectedHeader = this._protected
|
72 | const { length: recipientCount } = this._recipients
|
73 |
|
74 | const jweHeader = {
|
75 | ...protectedHeader,
|
76 | ...unprotectedHeader,
|
77 | ...recipient.header
|
78 | }
|
79 | const { key } = recipient
|
80 |
|
81 | const enc = jweHeader.enc
|
82 | let alg = jweHeader.alg
|
83 |
|
84 | if (key.use === 'sig') {
|
85 | throw new TypeError('a key with "use":"sig" is not usable for encryption')
|
86 | }
|
87 |
|
88 | if (alg === 'dir') {
|
89 | check(key, 'encrypt', enc)
|
90 | } else if (alg) {
|
91 | check(key, 'keyManagementEncrypt', alg)
|
92 | } else {
|
93 | alg = key.alg || [...key.algorithms('wrapKey')][0] || [...key.algorithms('deriveKey')][0]
|
94 |
|
95 | if (alg === 'ECDH-ES' && recipientCount !== 1) {
|
96 | alg = [...key.algorithms('deriveKey')][1]
|
97 | }
|
98 |
|
99 | if (!alg) {
|
100 | throw new JWEInvalid('could not resolve a usable "alg" for a recipient')
|
101 | }
|
102 |
|
103 | if (recipientCount === 1) {
|
104 | if (protectedHeader) {
|
105 | protectedHeader.alg = alg
|
106 | } else {
|
107 | this._protected = { alg }
|
108 | }
|
109 | } else {
|
110 | if (recipient.header) {
|
111 | recipient.header.alg = alg
|
112 | } else {
|
113 | recipient.header = { alg }
|
114 | }
|
115 | }
|
116 | }
|
117 |
|
118 | let wrapped
|
119 | let generatedHeader
|
120 |
|
121 | if (key.kty === 'oct' && alg === 'dir') {
|
122 | this._cek = importKey(key[KEYOBJECT], { use: 'enc', alg: enc })
|
123 | } else {
|
124 | check(this._cek, 'encrypt', enc)
|
125 | ;({ wrapped, header: generatedHeader } = keyManagementEncrypt(alg, key, this._cek[KEYOBJECT].export(), { enc, alg }))
|
126 | if (alg === 'ECDH-ES') {
|
127 | this._cek = importKey(createSecretKey(wrapped), { use: 'enc', alg: enc })
|
128 | }
|
129 | }
|
130 |
|
131 | if (alg === 'dir' || alg === 'ECDH-ES') {
|
132 | recipient.encrypted_key = ''
|
133 | } else {
|
134 | recipient.encrypted_key = base64url.encodeBuffer(wrapped)
|
135 | }
|
136 |
|
137 | if (generatedHeader) {
|
138 | recipient.generatedHeader = generatedHeader
|
139 | }
|
140 | }
|
141 |
|
142 | |
143 |
|
144 |
|
145 | encrypt (serialization) {
|
146 | const serializer = serializers[serialization]
|
147 | if (!serializer) {
|
148 | throw new TypeError('serialization must be one of "compact", "flattened", "general"')
|
149 | }
|
150 |
|
151 | if (!this._recipients.length) {
|
152 | throw new JWEInvalid('missing recipients')
|
153 | }
|
154 |
|
155 | serializer.validate(this._protected, this._unprotected, this._aad, this._recipients)
|
156 |
|
157 | let enc = validateHeaders(this._protected, this._unprotected, this._recipients, false, this._protected ? this._protected.crit : undefined)
|
158 | if (!enc) {
|
159 | enc = 'A128CBC-HS256'
|
160 | if (this._protected) {
|
161 | this._protected.enc = enc
|
162 | } else {
|
163 | this._protected = { enc }
|
164 | }
|
165 | }
|
166 | const final = {}
|
167 | this._cek = generateCEK(enc)
|
168 |
|
169 | for (const recipient of this._recipients) {
|
170 | this[PROCESS_RECIPIENT](recipient)
|
171 | }
|
172 |
|
173 | const iv = generateIV(enc)
|
174 | final.iv = base64url.encodeBuffer(iv)
|
175 |
|
176 | if (this._recipients.length === 1 && this._recipients[0].generatedHeader) {
|
177 | const [{ generatedHeader }] = this._recipients
|
178 | delete this._recipients[0].generatedHeader
|
179 | this._protected = {
|
180 | ...this._protected,
|
181 | ...generatedHeader
|
182 | }
|
183 | }
|
184 |
|
185 | if (this._protected) {
|
186 | final.protected = base64url.JSON.encode(this._protected)
|
187 | }
|
188 | final.unprotected = this._unprotected
|
189 |
|
190 | let aad
|
191 | if (this._aad) {
|
192 | final.aad = base64url.encode(this._aad)
|
193 | aad = Buffer.concat([Buffer.from(final.protected || ''), Buffer.from('.'), Buffer.from(final.aad)])
|
194 | } else {
|
195 | aad = Buffer.from(final.protected || '')
|
196 | }
|
197 |
|
198 | let cleartext = this._cleartext
|
199 | if (this._protected && 'zip' in this._protected) {
|
200 | cleartext = deflateRawSync(cleartext)
|
201 | }
|
202 |
|
203 | const { ciphertext, tag } = encrypt(enc, this._cek, cleartext, { iv, aad })
|
204 | final.tag = base64url.encodeBuffer(tag)
|
205 | final.ciphertext = base64url.encodeBuffer(ciphertext)
|
206 |
|
207 | return serializer(final, this._recipients)
|
208 | }
|
209 | }
|
210 |
|
211 | module.exports = Encrypt
|