001package com.nimbusds.jose.crypto;
002
003
004import java.io.UnsupportedEncodingException;
005import java.security.interfaces.RSAPrivateKey;
006
007import javax.crypto.SecretKey;
008
009import org.bouncycastle.util.Arrays;
010
011import com.nimbusds.jose.CompressionAlgorithm;
012import com.nimbusds.jose.DefaultJWEHeaderFilter;
013import com.nimbusds.jose.EncryptionMethod;
014import com.nimbusds.jose.JOSEException;
015import com.nimbusds.jose.JWEAlgorithm;
016import com.nimbusds.jose.JWEDecrypter;
017import com.nimbusds.jose.JWEHeaderFilter;
018import com.nimbusds.jose.ReadOnlyJWEHeader;
019import com.nimbusds.jose.util.Base64URL;
020import com.nimbusds.jose.util.DeflateUtils;
021
022
023/**
024 * RSA decrypter 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 * <p>Accepts all {@link com.nimbusds.jose.JWEHeader#getReservedParameterNames
044 * reserved JWE header parameters}. Modify the {@link #getJWEHeaderFilter
045 * header filter} properties to restrict the acceptable JWE algorithms, 
046 * encryption methods and header parameters, or to allow custom JWE header 
047 * parameters.
048 * 
049 * @author David Ortiz
050 * @author Vladimir Dzhuvinov
051 * @version $version$ (2013-03-27)
052 *
053 */
054public class RSADecrypter extends RSACryptoProvider implements JWEDecrypter {
055
056
057        /**
058         * The JWE header filter.
059         */
060        private final DefaultJWEHeaderFilter headerFilter;
061
062
063        /**
064         * The private RSA key.
065         */
066        private RSAPrivateKey privateKey;
067
068
069        /**
070         * Creates a new RSA decrypter.
071         *
072         * @param privateKey The private RSA key. Must not be {@code null}.
073         */
074        public RSADecrypter(final RSAPrivateKey privateKey) {
075
076                if (privateKey == null) {
077
078                        throw new IllegalArgumentException("The private RSA key must not be null");
079                }
080
081                this.privateKey = privateKey;
082
083                headerFilter = new DefaultJWEHeaderFilter(supportedAlgorithms(), supportedEncryptionMethods());
084        }
085
086
087        /**
088         * Gets the private RSA key.
089         *
090         * @return The private RSA key.
091         */
092        public RSAPrivateKey getPrivateKey() {
093
094                return privateKey;
095        }
096
097
098        @Override
099        public JWEHeaderFilter getJWEHeaderFilter() {
100
101                return headerFilter;
102        }
103
104
105        /**
106         * Applies decompression to the specified plain text if requested.
107         *
108         * @param readOnlyJWEHeader The JWE header. Must not be {@code null}.
109         * @param bytes             The plain text bytes. Must not be 
110         *                          {@code null}.
111         *
112         * @return The output bytes, decompressed if requested.
113         *
114         * @throws JOSEException If decompression failed or the requested 
115         *                       compression algorithm is not supported.
116         */
117        private static final byte[] applyDecompression(final ReadOnlyJWEHeader readOnlyJWEHeader, final byte[] bytes)
118                throws JOSEException {
119
120                CompressionAlgorithm compressionAlg = readOnlyJWEHeader.getCompressionAlgorithm();
121
122                if (compressionAlg == null) {
123
124                        return bytes;
125
126                } else if (compressionAlg.equals(CompressionAlgorithm.DEF)) {
127
128                        try {
129                                return DeflateUtils.decompress(bytes);
130
131                        } catch (Exception e) {
132
133                                throw new JOSEException("Couldn't decompress plain text: " + e.getMessage(), e);
134                        }
135
136                } else {
137
138                        throw new JOSEException("Unsupported compression algorithm: " + compressionAlg);
139                }
140        }
141
142
143        @Override
144        public byte[] decrypt(final ReadOnlyJWEHeader readOnlyJWEHeader,
145                              final Base64URL encryptedKey,
146                              final Base64URL iv,
147                              final Base64URL cipherText,
148                              final Base64URL integrityValue) 
149                throws JOSEException {
150
151                // Validate required JWE parts
152                if (encryptedKey == null) {
153
154                        throw new JOSEException("The encrypted key must not be null");
155                }       
156
157                if (iv == null) {
158
159                        throw new JOSEException("The initialization vector (IV) must not be null");
160                }
161
162                if (integrityValue == null) {
163
164                        throw new JOSEException("The integrity value must not be null");
165                }
166                
167
168                // Derive the encryption AES key
169                JWEAlgorithm alg = readOnlyJWEHeader.getAlgorithm();
170
171                SecretKey cmk = null;
172
173                if (alg.equals(JWEAlgorithm.RSA1_5)) {
174
175                        int keyLength = cmkBitLength(readOnlyJWEHeader.getEncryptionMethod());
176
177                        SecretKey randomCMK = AES.generateAESCMK(keyLength);
178
179                        try {
180                                cmk = RSA1_5.decryptCMK(privateKey, encryptedKey.decode(), keyLength);  
181                        
182                        } catch (Exception e) {
183
184                                // Protect against MMA attack by generating random CMK on failure, 
185                                // see http://www.ietf.org/mail-archive/web/jose/current/msg01832.html
186                                cmk = randomCMK;
187                        }
188                
189                } else if (alg.equals(JWEAlgorithm.RSA_OAEP)) {
190
191                        cmk = RSA_OAEP.decryptCMK(privateKey, encryptedKey.decode());
192
193                } else {
194                
195                        throw new JOSEException("Unsupported algorithm, must be RSA1_5 or RSA_OAEP");
196                }
197
198                EncryptionMethod enc = readOnlyJWEHeader.getEncryptionMethod();
199
200                byte[] plainText;
201
202                if (enc.equals(EncryptionMethod.A128CBC_HS256) || enc.equals(EncryptionMethod.A256CBC_HS512)    ) {
203
204                        byte[] epu = null;
205
206                        if (readOnlyJWEHeader.getEncryptionPartyUInfo() != null) {
207
208                                epu = readOnlyJWEHeader.getEncryptionPartyUInfo().decode();
209                        }
210
211                        byte[] epv = null;
212                        
213                        if (readOnlyJWEHeader.getEncryptionPartyVInfo() != null) {
214
215                                epv = readOnlyJWEHeader.getEncryptionPartyVInfo().decode();
216                        }
217
218                        SecretKey cek = ConcatKDF.generateCEK(cmk, enc, epu, epv);
219
220                        plainText = AESCBC.decrypt(cek, iv.decode(), cipherText.decode());
221
222                        SecretKey cik = ConcatKDF.generateCIK(cmk, enc, epu, epv);
223
224                        String macInput = readOnlyJWEHeader.toBase64URL().toString() + "." +
225                                          encryptedKey.toString() + "." +
226                                          iv.toString() + "." +
227                                          cipherText.toString();
228
229                        byte[] mac = HMAC.compute(cik, macInput.getBytes());
230
231                        if (! Arrays.constantTimeAreEqual(integrityValue.decode(), mac)) {
232
233                                throw new JOSEException("HMAC integrity check failed");
234                        }
235
236                } else if (enc.equals(EncryptionMethod.A128GCM) || enc.equals(EncryptionMethod.A256GCM)    ) {
237
238                        // Compose the additional authenticated data
239                        String authDataString = readOnlyJWEHeader.toBase64URL().toString() + "." +
240                                                encryptedKey.toString() + "." +
241                                                iv.toString();
242
243                        byte[] authData = null;
244
245                        try {
246                                authData = authDataString.getBytes("UTF-8");
247
248                        } catch (UnsupportedEncodingException e) {
249
250                                throw new JOSEException(e.getMessage(), e);
251                        }
252
253                        plainText = AESGCM.decrypt(cmk, iv.decode(), cipherText.decode(), authData, integrityValue.decode());
254
255                } else {
256
257                        throw new JOSEException("Unsupported encryption method, must be A128CBC_HS256, A256CBC_HS512, A128GCM or A128GCM");
258                }
259
260
261                // Apply decompression if requested
262                return applyDecompression(readOnlyJWEHeader, plainText);
263        }
264}
265