com.netflix.msl.crypto.JsonWebEncryptionCryptoContext.java Source code

Java tutorial

Introduction

Here is the source code for com.netflix.msl.crypto.JsonWebEncryptionCryptoContext.java

Source

/**
 * Copyright (c) 2013-2014 Netflix, Inc.  All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.msl.crypto;

import java.nio.charset.Charset;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.GCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;

import com.netflix.msl.MslCryptoException;
import com.netflix.msl.MslError;
import com.netflix.msl.MslInternalException;
import com.netflix.msl.util.JsonUtils;
import com.netflix.msl.util.MslContext;

/**
 * <p>This key exchange crypto context provides an implementation of the JSON
 * web encryption algorithm as defined in
 * <a href="http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-08">JSON Web Encryption</a>.
 * It supports a limited subset of the algorithms.</p>
 * 
 * @author Wesley Miaw <wmiaw@netflix.com>
 */
public class JsonWebEncryptionCryptoContext implements ICryptoContext {
    /** Encoding charset. */
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    /** JSON key recipients. */
    private static final String KEY_RECIPIENTS = "recipients";
    /** JSON key header. */
    private static final String KEY_HEADER = "header";
    /** JSON key encrypted key. */
    private static final String KEY_ENCRYPTED_KEY = "encrypted_key";
    /** JSON key integrity value. */
    private static final String KEY_INTEGRITY_VALUE = "integrity_value";
    /** JSON key initialization vector. */
    private static final String KEY_INITIALIZATION_VECTOR = "initialization_vector";
    /** JSON key ciphertext. */
    private static final String KEY_CIPHERTEXT = "ciphertext";

    /** JSON key wrap algorithm. */
    private static final String KEY_ALGORITHM = "alg";
    /** JSON key encryption algorithm. */
    private static final String KEY_ENCRYPTION = "enc";

    /** AES-128 GCM authentication tag length in bits. */
    private static final int A128_GCM_AT_LENGTH = 128;
    /** AES-128 GCM key length in bytes. */
    private static final int A128_GCM_KEY_LENGTH = 16;
    /** AES-128 GCM initialization vector length in bytes. */
    private static final int A128_GCM_IV_LENGTH = 12;

    /** AES-256 GCM authentication tag length in bits. */
    private static final int A256_GCM_AT_LENGTH = 128;
    /** AES-256 GCM key length in bytes. */
    private static final int A256_GCM_KEY_LENGTH = 32;
    /** AES-256 GCM initialization vector length in bytes. */
    private static final int A256_GCM_IV_LENGTH = 12;

    /** Supported content encryption key encryption algorithms. */
    private static enum Algorithm {
        /** RSAES-OAEP */
        RSA_OAEP("RSA-OAEP"),
        /** AES-128 Key Wrap */
        A128KW("A128KW");

        /**
         * @param name JSON Web Encryption algorithm name.
         */
        private Algorithm(final String name) {
            this.name = name;
        }

        /* (non-Javadoc)
         * @see java.lang.Enum#toString()
         */
        @Override
        public String toString() {
            return name;
        }

        /**
         * @param name JSON Web Encryption algorithm name.
         * @return the algorithm.
         * @throws IllegalArgumentException if the algorithm name is unknown.
         */
        public static Algorithm fromString(final String name) {
            for (final Algorithm algo : values()) {
                if (algo.toString().equals(name))
                    return algo;
            }
            throw new IllegalArgumentException("Algorithm " + name + " is unknown.");
        }

        /** JSON Web Encryption algorithm name. */
        private final String name;
    }

    /**
     * The Content Encryption Key crypto context is used to encrypt/decrypt the
     * randomly generated content encryption key.
     */
    public static abstract class CekCryptoContext implements ICryptoContext {
        /**
         * Create a new content encryption key crypto context with the
         * specified content encryption key encryption algorithm.
         * 
         * @param algo content encryption key encryption algorithm.
         */
        protected CekCryptoContext(final Algorithm algo) {
            this.algo = algo;
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#wrap(byte[])
         */
        @Override
        public byte[] wrap(byte[] data) throws MslCryptoException {
            throw new MslCryptoException(MslError.WRAP_NOT_SUPPORTED);
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#unwrap(byte[])
         */
        @Override
        public byte[] unwrap(byte[] data) throws MslCryptoException {
            throw new MslCryptoException(MslError.UNWRAP_NOT_SUPPORTED);
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#sign(byte[])
         */
        @Override
        public byte[] sign(byte[] data) throws MslCryptoException {
            throw new MslCryptoException(MslError.SIGN_NOT_SUPPORTED);
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#verify(byte[], byte[])
         */
        @Override
        public boolean verify(byte[] data, byte[] signature) throws MslCryptoException {
            throw new MslCryptoException(MslError.VERIFY_NOT_SUPPORTED);
        }

        /**
         * @return the content encryption key encryption algorithm.
         */
        Algorithm getAlgorithm() {
            return algo;
        }

        /** Content encryption key encryption algorithm. */
        private final Algorithm algo;
    }

    /**
     * RSA-OAEP encrypt/decrypt of the content encryption key.
     */
    public static class RsaOaepCryptoContext extends CekCryptoContext {
        /** RSA-OAEP cipher transform. */
        private static final String RSA_OAEP_TRANSFORM = "RSA/ECB/OAEPPadding";

        /**
         * <p>Create a new RSA crypto context for encrypt/decrypt using the
         * provided public and private keys. All other operations are
         * unsupported.</p>
         * 
         * <p>If there is no private key decryption is unsupported.</p>
         * 
         * <p>If there is no public key encryption is unsupported.</p>
         * 
         * @param privateKey the private key. May be null.
         * @param publicKey the public key. May be null.
         */
        public RsaOaepCryptoContext(final PrivateKey privateKey, final PublicKey publicKey) {
            super(Algorithm.RSA_OAEP);
            this.privateKey = privateKey;
            this.publicKey = publicKey;
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#encrypt(byte[])
         */
        @Override
        public byte[] encrypt(byte[] data) throws MslCryptoException {
            if (publicKey == null)
                throw new MslCryptoException(MslError.ENCRYPT_NOT_SUPPORTED, "no public key");
            Throwable reset = null;
            try {
                // Encrypt plaintext.
                final Cipher cipher = CryptoCache.getCipher(RSA_OAEP_TRANSFORM);
                cipher.init(Cipher.ENCRYPT_MODE, publicKey, OAEPParameterSpec.DEFAULT);
                return cipher.doFinal(data);
            } catch (final NoSuchPaddingException e) {
                reset = e;
                throw new MslInternalException("Unsupported padding exception.", e);
            } catch (final NoSuchAlgorithmException e) {
                reset = e;
                throw new MslInternalException("Invalid cipher algorithm specified.", e);
            } catch (final InvalidKeyException e) {
                reset = e;
                throw new MslCryptoException(MslError.INVALID_PUBLIC_KEY, e);
            } catch (final IllegalBlockSizeException e) {
                reset = e;
                throw new MslCryptoException(MslError.PLAINTEXT_ILLEGAL_BLOCK_SIZE,
                        "not expected when padding is specified", e);
            } catch (final BadPaddingException e) {
                reset = e;
                throw new MslCryptoException(MslError.PLAINTEXT_BAD_PADDING, "not expected when encrypting", e);
            } catch (final InvalidAlgorithmParameterException e) {
                reset = e;
                throw new MslCryptoException(MslError.INVALID_ALGORITHM_PARAMS, e);
            } catch (final RuntimeException e) {
                reset = e;
                throw e;
            } finally {
                // FIXME Remove this once BouncyCastle Cipher is fixed in v1.48+
                if (reset != null)
                    CryptoCache.resetCipher(RSA_OAEP_TRANSFORM);
            }
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#decrypt(byte[])
         */
        @Override
        public byte[] decrypt(byte[] data) throws MslCryptoException {
            if (privateKey == null)
                throw new MslCryptoException(MslError.DECRYPT_NOT_SUPPORTED, "no private key");
            Throwable reset = null;
            try {
                // Decrypt ciphertext.
                final Cipher cipher = CryptoCache.getCipher(RSA_OAEP_TRANSFORM);
                cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEPParameterSpec.DEFAULT);
                return cipher.doFinal(data);
            } catch (final NoSuchPaddingException e) {
                reset = e;
                throw new MslInternalException("Unsupported padding exception.", e);
            } catch (final NoSuchAlgorithmException e) {
                reset = e;
                throw new MslInternalException("Invalid cipher algorithm specified.", e);
            } catch (final InvalidKeyException e) {
                reset = e;
                throw new MslCryptoException(MslError.INVALID_PRIVATE_KEY, e);
            } catch (final IllegalBlockSizeException e) {
                reset = e;
                throw new MslCryptoException(MslError.CIPHERTEXT_ILLEGAL_BLOCK_SIZE, e);
            } catch (final BadPaddingException e) {
                reset = e;
                throw new MslCryptoException(MslError.CIPHERTEXT_BAD_PADDING, e);
            } catch (final InvalidAlgorithmParameterException e) {
                reset = e;
                throw new MslCryptoException(MslError.INVALID_ALGORITHM_PARAMS, e);
            } catch (final RuntimeException e) {
                reset = e;
                throw e;
            } finally {
                // FIXME Remove this once BouncyCastle Cipher is fixed in v1.48+
                if (reset != null)
                    CryptoCache.resetCipher(RSA_OAEP_TRANSFORM);
            }
        }

        /** Encryption/decryption cipher. */
        protected final PrivateKey privateKey;
        /** Sign/verify signature. */
        protected final PublicKey publicKey;
    }

    /**
     * AES key wrap encrypt/decrypt of the content encryption key.
     */
    public static class AesKwCryptoContext extends CekCryptoContext {
        /** AES key wrap cipher transform. */
        private static final String A128_KW_TRANSFORM = "AESWrap";

        /**
         * Create a new AES key wrap crypto context with the provided secret
         * key.
         * 
         * @param key AES secret key.
         */
        public AesKwCryptoContext(final SecretKey key) {
            super(Algorithm.A128KW);
            if (!key.getAlgorithm().equals(JcaAlgorithm.AESKW))
                throw new IllegalArgumentException("Secret key must be an " + JcaAlgorithm.AESKW + " key.");
            this.key = key;
            this.cryptoContext = null;
        }

        /**
         * Create a new AES key wrap crypto context backed by the provided
         * AES crypto context.
         * 
         * @param cryptoContext AES crypto context.
         */
        public AesKwCryptoContext(final ICryptoContext cryptoContext) {
            super(Algorithm.A128KW);
            this.key = null;
            this.cryptoContext = cryptoContext;
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#encrypt(byte[])
         */
        @Override
        public byte[] encrypt(final byte[] data) throws MslCryptoException {
            // If a secret key is provided use it.
            if (key != null) {
                try {
                    // Encrypt plaintext.
                    final Cipher cipher = CryptoCache.getCipher(A128_KW_TRANSFORM);
                    cipher.init(Cipher.WRAP_MODE, key);
                    // TODO: The key spec algorithm should be based on the JWE
                    // encryption algorithm. Right now that is always AES-GCM.
                    final Key secretKey = new SecretKeySpec(data, "AES");
                    return cipher.wrap(secretKey);
                } catch (final NoSuchPaddingException e) {
                    throw new MslInternalException("Unsupported padding exception.", e);
                } catch (final NoSuchAlgorithmException e) {
                    throw new MslInternalException("Invalid cipher algorithm specified.", e);
                } catch (final IllegalArgumentException e) {
                    throw new MslInternalException("Invalid content encryption key provided.", e);
                } catch (final InvalidKeyException e) {
                    throw new MslCryptoException(MslError.INVALID_SYMMETRIC_KEY, e);
                } catch (final IllegalBlockSizeException e) {
                    throw new MslCryptoException(MslError.PLAINTEXT_ILLEGAL_BLOCK_SIZE,
                            "not expected when padding is specified", e);
                }
            }

            // Otherwise use the backing crypto context.
            return cryptoContext.wrap(data);
        }

        /* (non-Javadoc)
         * @see com.netflix.msl.crypto.ICryptoContext#decrypt(byte[])
         */
        @Override
        public byte[] decrypt(final byte[] data) throws MslCryptoException {
            // If a secret key is provided use it.
            if (key != null) {
                try {
                    // Decrypt ciphertext.
                    final Cipher cipher = CryptoCache.getCipher(A128_KW_TRANSFORM);
                    cipher.init(Cipher.UNWRAP_MODE, key);
                    return cipher.unwrap(data, "AES", Cipher.SECRET_KEY).getEncoded();
                } catch (final NoSuchPaddingException e) {
                    throw new MslInternalException("Unsupported padding exception.", e);
                } catch (final NoSuchAlgorithmException e) {
                    throw new MslInternalException("Invalid cipher algorithm specified.", e);
                } catch (final InvalidKeyException e) {
                    throw new MslCryptoException(MslError.INVALID_SYMMETRIC_KEY, e);
                }
            }

            // Otherwise use the backing crypto context.
            return cryptoContext.unwrap(data);
        }

        /** AES secret key. */
        private final SecretKey key;
        /** AES crypto context. */
        private final ICryptoContext cryptoContext;
    }

    /** Supported plaintext encryption algorithms. */
    public static enum Encryption {
        /** AES-128 GCM */
        A128GCM,
        /** AES-256 GCM */
        A256GCM,
    }

    /** Support serialization formats. */
    public static enum Format {
        /**
         * <a href="http://tools.ietf.org/html/draft-jones-jose-jwe-json-serialization-04">JSON Web Encryption JSON Serialization (JWE-JS)</a>
         */
        JWE_JS,
        /**
         * <a href="http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-08">JSON Web Encryption Compact Serialization</a>
         */
        JWE_CS
    }

    /**
     * Create a new JSON web encryption crypto context with the provided
     * content encryption key crypto context and specified plaintext encryption
     * algorithm.
     * 
     * @param ctx MSL context.
     * @param cryptoContext content encryption key crypto context.
     * @param enc plaintext encryption algorithm.
     * @param format serialization format.
     */
    public JsonWebEncryptionCryptoContext(final MslContext ctx, final CekCryptoContext cryptoContext,
            final Encryption enc, final Format format) {
        this.ctx = ctx;
        this.cekCryptoContext = cryptoContext;
        this.algo = cryptoContext.getAlgorithm();
        this.enc = enc;
        this.format = format;
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#encrypt(byte[])
     */
    @Override
    public byte[] encrypt(final byte[] data) throws MslCryptoException {
        throw new MslCryptoException(MslError.ENCRYPT_NOT_SUPPORTED);
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#decrypt(byte[])
     */
    @Override
    public byte[] decrypt(final byte[] data) throws MslCryptoException {
        throw new MslCryptoException(MslError.DECRYPT_NOT_SUPPORTED);
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#wrap(byte[])
     */
    @Override
    public byte[] wrap(final byte[] data) throws MslCryptoException {
        // Create the header.
        final String header;
        try {
            header = new JSONStringer().object().key(KEY_ALGORITHM).value(algo.toString()).key(KEY_ENCRYPTION)
                    .value(enc.name()).endObject().toString();
        } catch (final JSONException e) {
            throw new MslCryptoException(MslError.JWE_ENCODE_ERROR, e);
        }

        // Determine algorithm byte lengths.
        final int keylen, ivlen, atlen;
        if (Encryption.A128GCM.equals(enc)) {
            keylen = A128_GCM_KEY_LENGTH;
            ivlen = A128_GCM_IV_LENGTH;
            atlen = A128_GCM_AT_LENGTH;
        } else if (Encryption.A256GCM.equals(enc)) {
            keylen = A256_GCM_KEY_LENGTH;
            ivlen = A256_GCM_IV_LENGTH;
            atlen = A256_GCM_AT_LENGTH;
        } else {
            throw new MslCryptoException(MslError.UNSUPPORTED_JWE_ALGORITHM, enc.name());
        }

        // Generate the key and IV.
        final Random random = ctx.getRandom();
        final byte[] key = new byte[keylen];
        random.nextBytes(key);
        final KeyParameter cek = new KeyParameter(key);
        final byte[] iv = new byte[ivlen];
        random.nextBytes(iv);

        // Encrypt the CEK.
        final byte[] ecek = cekCryptoContext.encrypt(cek.getKey());

        // Base64-encode the data.
        final String headerB64 = JsonUtils.b64urlEncode(header.getBytes(UTF_8));
        final String ecekB64 = JsonUtils.b64urlEncode(ecek);
        final String ivB64 = JsonUtils.b64urlEncode(iv);

        // Create additional authenticated data.
        final String aad = headerB64 + "." + ecekB64 + "." + ivB64;

        // TODO: AES-GCM is not available via the JCE.
        //
        // Create and initialize the cipher for encryption.
        final GCMBlockCipher plaintextCipher = new GCMBlockCipher(new AESEngine());
        final AEADParameters params = new AEADParameters(cek, atlen, iv, aad.getBytes(UTF_8));
        plaintextCipher.init(true, params);

        // Encrypt the plaintext.
        final byte[] ciphertextATag;
        try {
            final int clen = plaintextCipher.getOutputSize(data.length);
            ciphertextATag = new byte[clen];
            // Encrypt the plaintext and get the resulting ciphertext length
            // which will be used for the authentication tag offset.
            final int offset = plaintextCipher.processBytes(data, 0, data.length, ciphertextATag, 0);
            // Append the authentication tag.
            plaintextCipher.doFinal(ciphertextATag, offset);
        } catch (final IllegalStateException e) {
            throw new MslCryptoException(MslError.WRAP_ERROR, e);
        } catch (final InvalidCipherTextException e) {
            throw new MslInternalException("Invalid ciphertext not expected when encrypting.", e);
        }

        // Split the result into the ciphertext and authentication tag.
        final byte[] ciphertext = Arrays.copyOfRange(ciphertextATag, 0, ciphertextATag.length - atlen / Byte.SIZE);
        final byte[] at = Arrays.copyOfRange(ciphertextATag, ciphertext.length, ciphertextATag.length);

        // Base64-encode the ciphertext and authentication tag.
        final String ciphertextB64 = JsonUtils.b64urlEncode(ciphertext);
        final String atB64 = JsonUtils.b64urlEncode(at);

        // Envelope the data.
        switch (format) {
        case JWE_CS: {
            final String serialization = aad + "." + ciphertextB64 + "." + atB64;
            return serialization.getBytes(UTF_8);
        }
        case JWE_JS: {
            try {
                // Create recipients array.
                final JSONArray recipients = new JSONArray();
                final JSONObject recipient = new JSONObject();
                recipient.put(KEY_HEADER, headerB64);
                recipient.put(KEY_ENCRYPTED_KEY, ecekB64);
                recipient.put(KEY_INTEGRITY_VALUE, atB64);
                recipients.put(recipient);

                // Create JSON serialization.
                final JSONObject serialization = new JSONObject();
                serialization.put(KEY_RECIPIENTS, recipients);
                serialization.put(KEY_INITIALIZATION_VECTOR, ivB64);
                serialization.put(KEY_CIPHERTEXT, ciphertextB64);
                return serialization.toString().getBytes(UTF_8);
            } catch (final JSONException e) {
                throw new MslCryptoException(MslError.JWE_ENCODE_ERROR, e);
            }
        }
        default:
            throw new MslCryptoException(MslError.UNSUPPORTED_JWE_SERIALIZATION, format.name());
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#unwrap(byte[])
     */
    @Override
    public byte[] unwrap(final byte[] data) throws MslCryptoException {
        // Parse the serialization.
        final String serialization = new String(data, UTF_8);
        final String headerB64, ecekB64, ivB64;
        final byte[] ciphertext, at;
        if (data[0] == '{') {
            try {
                final JSONObject serializationJo = new JSONObject(serialization);
                ivB64 = serializationJo.getString(KEY_INITIALIZATION_VECTOR);
                ciphertext = JsonUtils.b64urlDecode(serializationJo.getString(KEY_CIPHERTEXT));

                // TODO: For now, we only support one recipient.
                final JSONArray recipients = serializationJo.getJSONArray(KEY_RECIPIENTS);
                final JSONObject recipient = recipients.getJSONObject(0);
                headerB64 = recipient.getString(KEY_HEADER);
                ecekB64 = recipient.getString(KEY_ENCRYPTED_KEY);
                at = JsonUtils.b64urlDecode(recipient.getString(KEY_INTEGRITY_VALUE));
            } catch (final JSONException e) {
                throw new MslCryptoException(MslError.JWE_PARSE_ERROR, serialization, e);
            }
        } else {
            // Separate the compact serialization.
            final String[] parts = serialization.split("\\.");
            if (parts.length != 5)
                throw new MslCryptoException(MslError.JWE_PARSE_ERROR, serialization);

            // Extract the data from the serialization.
            headerB64 = parts[0];
            ecekB64 = parts[1];
            ivB64 = parts[2];
            ciphertext = JsonUtils.b64urlDecode(parts[3]);
            at = JsonUtils.b64urlDecode(parts[4]);
        }

        // Decode header, encrypted content encryption key, and IV.
        final byte[] headerBytes = JsonUtils.b64urlDecode(headerB64);
        final byte[] ecek = JsonUtils.b64urlDecode(ecekB64);
        final byte[] iv = JsonUtils.b64urlDecode(ivB64);

        // Verify data.
        if (headerBytes == null || headerBytes.length == 0 || ecek == null || ecek.length == 0 || iv == null
                || iv.length == 0 || ciphertext == null || ciphertext.length == 0 || at == null || at.length == 0) {
            throw new MslCryptoException(MslError.JWE_PARSE_ERROR, serialization);
        }

        // Reconstruct and parse the header.
        final String header = new String(headerBytes, UTF_8);
        final Algorithm algo;
        final Encryption enc;
        try {
            final JSONObject headerJo = new JSONObject(header);
            final String algoName = headerJo.getString(KEY_ALGORITHM);
            try {
                algo = Algorithm.fromString(algoName);
            } catch (final IllegalArgumentException e) {
                throw new MslCryptoException(MslError.JWE_PARSE_ERROR, algoName, e);
            }
            final String encName = headerJo.getString(KEY_ENCRYPTION);
            try {
                enc = Encryption.valueOf(encName);
            } catch (final IllegalArgumentException e) {
                throw new MslCryptoException(MslError.JWE_PARSE_ERROR, encName, e);
            }
        } catch (final JSONException e) {
            throw new MslCryptoException(MslError.JWE_PARSE_ERROR, header, e);
        }

        // Confirm header matches.
        if (!this.algo.equals(algo) || !this.enc.equals(enc))
            throw new MslCryptoException(MslError.JWE_ALGORITHM_MISMATCH, header);

        // Decrypt the CEK.
        final KeyParameter cek;
        try {
            final byte[] cekBytes = cekCryptoContext.decrypt(ecek);
            cek = new KeyParameter(cekBytes);
        } catch (final ArrayIndexOutOfBoundsException e) {
            // Thrown if the encrypted content encryption key is an invalid
            // length.
            throw new MslCryptoException(MslError.INVALID_SYMMETRIC_KEY, e);
        }

        // Create additional authenticated data.
        final String aad = headerB64 + "." + ecekB64 + "." + ivB64;

        // Determine algorithm byte lengths.
        final int keylen, atlen;
        if (Encryption.A128GCM.equals(enc)) {
            keylen = A128_GCM_KEY_LENGTH;
            atlen = A128_GCM_AT_LENGTH;
        } else if (Encryption.A256GCM.equals(enc)) {
            keylen = A256_GCM_KEY_LENGTH;
            atlen = A256_GCM_AT_LENGTH;
        } else {
            throw new MslCryptoException(MslError.UNSUPPORTED_JWE_ALGORITHM, enc.name());
        }

        // Verify algorithm parameters.
        if (cek.getKey().length != keylen)
            throw new MslCryptoException(MslError.INVALID_SYMMETRIC_KEY,
                    "content encryption key length: " + cek.getKey().length);
        if (at.length != atlen / Byte.SIZE)
            throw new MslCryptoException(MslError.INVALID_ALGORITHM_PARAMS,
                    "authentication tag length: " + at.length);

        // TODO: AES-GCM is not available via the JCE.
        //
        // Create and initialize the cipher for decryption.
        final GCMBlockCipher plaintextCipher = new GCMBlockCipher(new AESEngine());
        final AEADParameters params = new AEADParameters(cek, atlen, iv, aad.getBytes(UTF_8));
        plaintextCipher.init(false, params);

        // Decrypt the ciphertext.
        try {
            // Reconstruct the ciphertext and authentication tag.
            final byte[] ciphertextAtag = Arrays.copyOf(ciphertext, ciphertext.length + at.length);
            System.arraycopy(at, 0, ciphertextAtag, ciphertext.length, at.length);
            int plen = plaintextCipher.getOutputSize(ciphertextAtag.length);
            byte[] plaintext = new byte[plen];
            // Decrypt the ciphertext and get the resulting plaintext length
            // which will be used for the authentication tag offset.
            int offset = plaintextCipher.processBytes(ciphertextAtag, 0, ciphertextAtag.length, plaintext, 0);
            // Verify the authentication tag.
            plaintextCipher.doFinal(plaintext, offset);
            return plaintext;
        } catch (final IllegalStateException e) {
            throw new MslCryptoException(MslError.UNWRAP_ERROR, e);
        } catch (final InvalidCipherTextException e) {
            throw new MslCryptoException(MslError.UNWRAP_ERROR, e);
        } catch (final ArrayIndexOutOfBoundsException e) {
            // Thrown if the ciphertext is an invalid length.
            throw new MslCryptoException(MslError.UNWRAP_ERROR, e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#sign(byte[])
     */
    @Override
    public byte[] sign(final byte[] data) throws MslCryptoException {
        throw new MslCryptoException(MslError.SIGN_NOT_SUPPORTED);
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#verify(byte[], byte[])
     */
    @Override
    public boolean verify(final byte[] data, final byte[] signature) throws MslCryptoException {
        throw new MslCryptoException(MslError.VERIFY_NOT_SUPPORTED);
    }

    /** MSL context. */
    private final MslContext ctx;
    /** Content encryption key crypto context. */
    private final ICryptoContext cekCryptoContext;
    /** Wrap algorithm. */
    private final Algorithm algo;
    /** Encryption algorithm. */
    private final Encryption enc;
    /** Serialization format. */
    private final Format format;
}