001/*
002 * nimbus-jose-jwt
003 *
004 * Copyright 2012-2016, Connect2id Ltd.
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use
007 * this file except in compliance with the License. You may obtain a copy of the
008 * License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software distributed
013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the
015 * specific language governing permissions and limitations under the License.
016 */
017
018package com.nimbusds.jose.crypto;
019
020import com.nimbusds.jose.*;
021import com.nimbusds.jose.crypto.impl.AAD;
022import com.nimbusds.jose.crypto.impl.JWEHeaderValidation;
023import com.nimbusds.jose.crypto.impl.MultiCryptoProvider;
024import com.nimbusds.jose.jwk.JWK;
025import com.nimbusds.jose.jwk.JWKSet;
026import com.nimbusds.jose.jwk.KeyType;
027import com.nimbusds.jose.util.Base64URL;
028import com.nimbusds.jose.util.JSONArrayUtils;
029import com.nimbusds.jose.util.JSONObjectUtils;
030import net.jcip.annotations.ThreadSafe;
031
032import javax.crypto.SecretKey;
033import java.util.List;
034import java.util.Map;
035
036
037/**
038 * Multi-recipient encrypter of {@link com.nimbusds.jose.JWEObjectJSON JWE
039 * objects}.
040 *
041 * <p>This class is thread-safe.
042 *
043 * <p>Supports the following key management algorithms:
044 *
045 * <ul>
046 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A128KW}
047 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A192KW}
048 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A256KW}
049 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A128GCMKW}
050 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A192GCMKW}
051 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#A256GCMKW}
052 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#DIR}
053 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#ECDH_ES_A128KW}
054 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#ECDH_ES_A192KW}
055 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#ECDH_ES_A256KW}
056 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA_OAEP_256}
057 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA_OAEP_384}
058 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA_OAEP_512}
059 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA_OAEP} (deprecated)
060 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA1_5} (deprecated)
061 * </ul>
062 *
063 * <p>Supports the following elliptic curves:
064 *
065 * <ul>
066 *     <li>{@link com.nimbusds.jose.jwk.Curve#P_256}
067 *     <li>{@link com.nimbusds.jose.jwk.Curve#P_384}
068 *     <li>{@link com.nimbusds.jose.jwk.Curve#P_521}
069 *     <li>{@link com.nimbusds.jose.jwk.Curve#X25519} (Curve25519)
070 * </ul>
071 *
072 * <p>Supports the following content encryption algorithms:
073 *
074 * <ul>
075 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A128CBC_HS256} (requires 256 bit key)
076 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A192CBC_HS384} (requires 384 bit key)
077 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A256CBC_HS512} (requires 512 bit key)
078 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A128GCM} (requires 128 bit key)
079 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A192GCM} (requires 192 bit key)
080 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A256GCM} (requires 256 bit key)
081 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A128CBC_HS256_DEPRECATED} (requires 256 bit key)
082 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A256CBC_HS512_DEPRECATED} (requires 512 bit key)
083 *     <li>{@link com.nimbusds.jose.EncryptionMethod#XC20P} (requires 256 bit key)
084 * </ul>
085 *
086 * @author Egor Puzanov
087 * @author Vladimir Dzhuvinov
088 * @version 2023-09-10
089 */
090@ThreadSafe
091public class MultiEncrypter extends MultiCryptoProvider implements JWEEncrypter {
092
093
094        /**
095         * Common JWK and JWEHeader parameters.
096         */
097        private static final String[] RECIPIENT_HEADER_PARAMS = {
098                HeaderParameterNames.KEY_ID,
099                HeaderParameterNames.ALGORITHM,
100                HeaderParameterNames.X_509_CERT_URL,
101                HeaderParameterNames.X_509_CERT_SHA_1_THUMBPRINT,
102                HeaderParameterNames.X_509_CERT_SHA_256_THUMBPRINT,
103                HeaderParameterNames.X_509_CERT_CHAIN
104        };
105
106
107        /**
108         * The JWK public keys.
109         */
110        private final JWKSet keys;
111
112
113        /**
114         * Creates a new multi-recipient encrypter.
115         *
116         * @param keys The keys to encrypt to. Must not be {@code null}.
117         *
118         * @throws KeyLengthException If the symmetric key length is not
119         *                            compatible.
120         */
121        public MultiEncrypter(final JWKSet keys)
122                throws KeyLengthException {
123
124                this(keys, findDirectCEK(keys));
125        }
126
127
128        /**
129         * Creates a new multi-recipient encrypter.
130         *
131         * @param keys                 The keys to encrypt to. Must not be
132         *                             {@code null}.
133         * @param contentEncryptionKey The content encryption key (CEK) to use.
134         *                             If specified its algorithm must be "AES"
135         *                             or "ChaCha20" and its length must match
136         *                             the expected for the JWE encryption
137         *                             method ("enc"). If {@code null} a CEK
138         *                             will be generated for each JWE.
139         *
140         * @throws KeyLengthException If the symmetric key length is not
141         *                            compatible.
142         */
143        public MultiEncrypter(final JWKSet keys, final SecretKey contentEncryptionKey)
144                throws KeyLengthException {
145                
146                super(contentEncryptionKey);
147
148                if (keys == null) {
149                        throw new IllegalArgumentException("The JWK set must not be null");
150                }
151
152                for (JWK jwk : keys.getKeys()) {
153                        KeyType kty = jwk.getKeyType();
154                        if (jwk.getAlgorithm() == null) {
155                                throw new IllegalArgumentException("Each JWK must specify a key encryption algorithm");
156                        }
157                        JWEAlgorithm alg = JWEAlgorithm.parse(jwk.getAlgorithm().toString());
158                        if (JWEAlgorithm.DIR.equals(alg)
159                                        && KeyType.OCT.equals(kty)
160                                        && !jwk.toOctetSequenceKey().toSecretKey("AES").equals(contentEncryptionKey)) {
161                                throw new IllegalArgumentException("Bad CEK");
162                        }
163                        if (!((KeyType.RSA.equals(kty) && RSAEncrypter.SUPPORTED_ALGORITHMS.contains(alg))
164                                        || (KeyType.EC.equals(kty) && ECDHEncrypter.SUPPORTED_ALGORITHMS.contains(alg))
165                                        || (KeyType.OCT.equals(kty) && AESEncrypter.SUPPORTED_ALGORITHMS.contains(alg))
166                                        || (KeyType.OCT.equals(kty) && DirectEncrypter.SUPPORTED_ALGORITHMS.contains(alg))
167                                        || (KeyType.OKP.equals(kty) && X25519Encrypter.SUPPORTED_ALGORITHMS.contains(alg)))) {
168                                throw new IllegalArgumentException("Unsupported key encryption algorithm: " + alg);
169                        }
170                }
171
172                this.keys = keys;
173        }
174
175
176        /**
177         * Returns the {@link SecretKey} of the recipients with
178         * {@link JWEAlgorithm#DIR} if present.
179         *
180         * @param keys The public keys. Must not be {@code null}.
181         *
182         * @return The SecretKey.
183         */
184        private static SecretKey findDirectCEK(final JWKSet keys) {
185                if (keys != null) {
186                        for (JWK jwk : keys.getKeys()) {
187                                if (JWEAlgorithm.DIR.equals(jwk.getAlgorithm()) && KeyType.OCT.equals(jwk.getKeyType())) {
188                                        return jwk.toOctetSequenceKey().toSecretKey("AES");
189                                }
190                        }
191                }
192                return null;
193        }
194
195
196        /**
197         * Encrypts the specified clear text of a {@link JWEObject JWE object}.
198         *
199         * @param header    The JSON Web Encryption (JWE) header. Must specify
200         *                  a supported JWE algorithm and method. Must not be
201         *                  {@code null}.
202         * @param clearText The clear text to encrypt. Must not be {@code null}.
203         *
204         * @return The resulting JWE crypto parts.
205         *
206         * @throws JOSEException If the JWE algorithm or method is not
207         *                       supported or if encryption failed for some
208         *                       other internal reason.
209         */
210        @Deprecated
211        public JWECryptoParts encrypt(final JWEHeader header, final byte[] clearText)
212                throws JOSEException {
213
214                return encrypt(header, clearText, AAD.compute(header));
215        }
216
217
218        @Override
219        public JWECryptoParts encrypt(final JWEHeader header, final byte[] clearText, final byte[] aad)
220                throws JOSEException {
221
222                if (aad == null) {
223                        throw new JOSEException("Missing JWE additional authenticated data (AAD)");
224                }
225
226                final EncryptionMethod enc = header.getEncryptionMethod();
227                final SecretKey cek = getCEK(enc);
228
229                JWECryptoParts jweParts;
230                JWEEncrypter encrypter;
231                JWEHeader recipientHeader = null;
232                Base64URL encryptedKey = null;
233                Base64URL cipherText = null;
234                Base64URL iv = null;
235                Base64URL tag = null;
236                JWEAlgorithm alg;
237                Payload payload = new Payload(clearText);
238                List<Object> recipients = JSONArrayUtils.newJSONArray();
239
240                for (JWK key : keys.getKeys()) {
241                        KeyType kty = key.getKeyType();
242
243                        // build JWEHeader from protected header and recipients public key parameters
244                        Map<String, Object> keyMap = key.toJSONObject();
245                        UnprotectedHeader.Builder unprotected = new UnprotectedHeader.Builder();
246                        for (String param : RECIPIENT_HEADER_PARAMS) {
247                                if (keyMap.containsKey(param)) {
248                                        unprotected.param(param, keyMap.get(param));
249                                }
250                        }
251
252                        // create recipients JWEObject, select encrypter and encrypt the payload.
253                        try {
254                                recipientHeader = (JWEHeader) header.join(unprotected.build());
255                        } catch (Exception e) {
256                                throw new JOSEException(e.getMessage(), e);
257                        }
258                        alg = JWEHeaderValidation.getAlgorithmAndEnsureNotNull(recipientHeader);
259
260                        if (KeyType.RSA.equals(kty) && RSAEncrypter.SUPPORTED_ALGORITHMS.contains(alg)) {
261                                encrypter = new RSAEncrypter(key.toRSAKey().toRSAPublicKey(), cek);
262                        } else if (KeyType.EC.equals(kty) && ECDHEncrypter.SUPPORTED_ALGORITHMS.contains(alg)) {
263                                encrypter = new ECDHEncrypter(key.toECKey().toECPublicKey(), cek);
264                        } else if (KeyType.OCT.equals(kty) && AESEncrypter.SUPPORTED_ALGORITHMS.contains(alg)) {
265                                encrypter = new AESEncrypter(key.toOctetSequenceKey().toSecretKey("AES"), cek);
266                        } else if (KeyType.OCT.equals(kty) && DirectEncrypter.SUPPORTED_ALGORITHMS.contains(alg)) {
267                                encrypter = new DirectEncrypter(key.toOctetSequenceKey().toSecretKey("AES"));
268                        } else if (KeyType.OKP.equals(kty) && X25519Encrypter.SUPPORTED_ALGORITHMS.contains(alg)) {
269                                encrypter = new X25519Encrypter(key.toOctetKeyPair().toPublicJWK(), cek);
270                        } else {
271                                continue;
272                        }
273                        jweParts = encrypter.encrypt(recipientHeader, payload.toBytes(), aad);
274
275                        // build recipients header object by removing protected header params from recipients JWEHeader
276                        Map<String, Object> recipientHeaderMap = jweParts.getHeader().toJSONObject();
277                        for (String param : header.getIncludedParams()) {
278                                recipientHeaderMap.remove(param);
279                        }
280                        Map<String, Object> recipient = JSONObjectUtils.newJSONObject();
281                        recipient.put("header", recipientHeaderMap);
282
283                        // do not put symmetric keys into JWE JSON object
284                        if (!JWEAlgorithm.DIR.equals(alg)) {
285                                recipient.put("encrypted_key", jweParts.getEncryptedKey().toString());
286                        }
287                        recipients.add(recipient);
288
289                        // update the iv, cipherText and tag parameters only after first round. Set payload to empty string.
290                        if (recipients.size() == 1) {
291                                payload = new Payload("");
292                                encryptedKey = jweParts.getEncryptedKey();
293                                iv = jweParts.getInitializationVector();
294                                cipherText = jweParts.getCipherText();
295                                tag = jweParts.getAuthenticationTag();
296                        }
297                }
298                if (recipients.size() > 1) {
299                        Map<String, Object> jweJsonObject = JSONObjectUtils.newJSONObject();
300                        jweJsonObject.put("recipients", recipients);
301                        encryptedKey = Base64URL.encode(JSONObjectUtils.toJSONString(jweJsonObject));
302                }
303                return new JWECryptoParts(header, encryptedKey, iv, cipherText, tag);
304        }
305}