001package com.nimbusds.jose.crypto;
002
003
004import java.io.UnsupportedEncodingException;
005import java.security.NoSuchAlgorithmException;
006import java.security.SecureRandom;
007import java.security.interfaces.RSAPublicKey;
008
009import javax.crypto.SecretKey;
010
011import com.nimbusds.jose.CompressionAlgorithm;
012import com.nimbusds.jose.EncryptionMethod;
013import com.nimbusds.jose.JOSEException;
014import com.nimbusds.jose.JWEAlgorithm;
015import com.nimbusds.jose.JWECryptoParts;
016import com.nimbusds.jose.JWEEncrypter;
017import com.nimbusds.jose.ReadOnlyJWEHeader;
018import com.nimbusds.jose.util.Base64URL;
019import com.nimbusds.jose.util.DeflateUtils;
020
021
022
023/**
024 * RSA encrypter of {@link com.nimbusds.jose.JWEObject JWE objects}. This class
025 * is thread-safe.
026 *
027 * <p>Supports the following JWE algorithms:
028 *
029 * <ul>
030 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA1_5}
031 *     <li>{@link com.nimbusds.jose.JWEAlgorithm#RSA_OAEP}
032 * </ul>
033 *
034 * <p>Supports the following encryption methods:
035 *
036 * <ul>
037 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A128CBC_HS256}
038 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A256CBC_HS512}
039 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A128GCM}
040 *     <li>{@link com.nimbusds.jose.EncryptionMethod#A256GCM}
041 * </ul>
042 *
043 * @author David Ortiz
044 * @author Vladimir Dzhuvinov
045 * @version $version$ (2013-03-26)
046 */
047public class RSAEncrypter extends RSACryptoProvider implements JWEEncrypter {
048
049
050        /**
051         * Random byte generator.
052         */
053        private final SecureRandom randomGen;
054
055
056        /**
057         * The public RSA key.
058         */
059        private final RSAPublicKey publicKey;
060
061
062        /**
063         * Creates a new RSA encrypter.
064         *
065         * @param publicKey The public RSA key. Must not be {@code null}.
066         *
067         * @throws JOSEException If the underlying secure random generator
068         *                       couldn't be instantiated.
069         */
070        public RSAEncrypter(final RSAPublicKey publicKey)
071                throws JOSEException {
072
073                if (publicKey == null) {
074
075                        throw new IllegalArgumentException("The public RSA key must not be null");
076                }
077
078                this.publicKey = publicKey;
079
080
081                try {
082                        randomGen = SecureRandom.getInstance("SHA1PRNG");
083
084                } catch(NoSuchAlgorithmException e) {
085
086                        throw new JOSEException(e.getMessage(), e);
087                }
088        }
089
090
091        /**
092         * Gets the public RSA key.
093         *
094         * @return The public RSA key.
095         */
096        public RSAPublicKey getPublicKey() {
097
098                return publicKey;
099        }
100
101
102        /**
103         * Applies compression to the specified plain text if requested.
104         *
105         * @param readOnlyJWEHeader The JWE header. Must not be {@code null}.
106         * @param bytes             The plain text bytes. Must not be 
107         *                          {@code null}.
108         *
109         * @return The bytes to encrypt.
110         *
111         * @throws JOSEException If compression failed or the requested 
112         *                       compression algorithm is not supported.
113         */
114        private static final byte[] applyCompression(final ReadOnlyJWEHeader readOnlyJWEHeader, final byte[] bytes)
115                throws JOSEException {
116
117                CompressionAlgorithm compressionAlg = readOnlyJWEHeader.getCompressionAlgorithm();
118
119                if (compressionAlg == null) {
120
121                        return bytes;
122
123                } else if (compressionAlg.equals(CompressionAlgorithm.DEF)) {
124
125                        try {
126                                return DeflateUtils.compress(bytes);
127
128                        } catch (Exception e) {
129
130                                throw new JOSEException("Couldn't compress plain text: " + e.getMessage(), e);
131                        }
132
133                } else {
134
135                        throw new JOSEException("Unsupported compression algorithm: " + compressionAlg);
136                }
137        }
138
139
140        @Override
141        public JWECryptoParts encrypt(final ReadOnlyJWEHeader readOnlyJWEHeader, final byte[] bytes)
142                throws JOSEException {
143
144                JWEAlgorithm alg = readOnlyJWEHeader.getAlgorithm();
145                EncryptionMethod enc = readOnlyJWEHeader.getEncryptionMethod();
146
147                // Generate and encrypt the CMK according to the JWE alg
148                final int keyLength = RSACryptoProvider.cmkBitLength(enc);
149
150                SecretKey cmk = AES.generateAESCMK(keyLength);
151
152                Base64URL encryptedKey = null; // The second JWE part
153
154                if (alg.equals(JWEAlgorithm.RSA1_5)) {
155
156                        encryptedKey = Base64URL.encode(RSA1_5.encryptCMK(publicKey, cmk));
157
158                } else if (alg.equals(JWEAlgorithm.RSA_OAEP)) {
159
160                        encryptedKey = Base64URL.encode(RSA_OAEP.encryptCMK(publicKey, cmk));
161
162                } else {
163
164                        throw new JOSEException("Unsupported algorithm, must be RSA1_5 or RSA_OAEP");
165                }
166
167                if (encryptedKey == null ) {
168
169                        throw new JOSEException("Couldn't generate encrypted key");
170                }
171
172
173                // Apply compression if instructed
174                byte[] plainText = applyCompression(readOnlyJWEHeader, bytes);
175                
176
177                // Encrypt the plain text according to the JWE enc
178                if (enc.equals(EncryptionMethod.A128CBC_HS256) || enc.equals(EncryptionMethod.A256CBC_HS512)) {
179
180                        byte[] epu = null;
181
182                        if (readOnlyJWEHeader.getEncryptionPartyUInfo() != null) {
183
184                                epu = readOnlyJWEHeader.getEncryptionPartyUInfo().decode();
185                        }
186
187                        byte[] epv = null;
188                        
189                        if (readOnlyJWEHeader.getEncryptionPartyVInfo() != null) {
190
191                                epv = readOnlyJWEHeader.getEncryptionPartyVInfo().decode();
192                        }
193
194                        SecretKey cek = ConcatKDF.generateCEK(cmk, enc, epu, epv);
195
196                        byte[] iv = AESCBC.generateIV(randomGen);
197
198                        byte[] cipherText = AESCBC.encrypt(cek, iv, plainText);
199
200                        SecretKey cik = ConcatKDF.generateCIK(cmk, enc, epu, epv);
201
202                        String macInput = readOnlyJWEHeader.toBase64URL().toString() + "." +
203                                          encryptedKey.toString() + "." +
204                                          Base64URL.encode(iv).toString() + "." +
205                                          Base64URL.encode(cipherText);
206
207                        byte[] mac = HMAC.compute(cik, macInput.getBytes());
208
209                        return new JWECryptoParts(encryptedKey,  
210                                                  Base64URL.encode(iv), 
211                                                  Base64URL.encode(cipherText),
212                                                  Base64URL.encode(mac));
213
214                } else if (enc.equals(EncryptionMethod.A128GCM) || enc.equals(EncryptionMethod.A256GCM)) {
215
216                        byte[] iv = AESGCM.generateIV(randomGen);
217
218                        // Compose the additional authenticated data
219                        String authDataString = readOnlyJWEHeader.toBase64URL().toString() + "." +
220                                                encryptedKey.toString() + "." +
221                                                Base64URL.encode(iv).toString();
222
223                        byte[] authData;
224
225                        try {
226                                authData = authDataString.getBytes("UTF-8");
227
228                        } catch (UnsupportedEncodingException e) {
229
230                                throw new JOSEException(e.getMessage(), e);
231                        }
232
233                        
234                        AESGCM.Result result = AESGCM.encrypt(cmk, iv, plainText, authData);
235
236                        return new JWECryptoParts(encryptedKey,  
237                                                  Base64URL.encode(iv), 
238                                                  Base64URL.encode(result.getCipherText()),
239                                                  Base64URL.encode(result.getAuthenticationTag()));
240
241                } else {
242
243                        throw new JOSEException("Unsupported encryption method, must be A128CBC_HS256, A256CBC_HS512, A128GCM or A128GCM");
244                }
245        }
246}