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

Java tutorial

Introduction

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

Source

/**
 * Copyright (c) 2012-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.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.macs.CMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.json.JSONException;
import org.json.JSONObject;

import com.netflix.msl.MslConstants;
import com.netflix.msl.MslCryptoException;
import com.netflix.msl.MslEncodingException;
import com.netflix.msl.MslError;
import com.netflix.msl.MslInternalException;
import com.netflix.msl.util.MslContext;
import com.netflix.msl.util.MslUtils;

/**
 * A symmetric crypto context performs AES-128 encryption/decryption, AES-128
 * key wrap/unwrap, and HMAC-SHA256 or AES-CMAC sign/verify.
 * 
 * @author Wesley Miaw <wmiaw@netflix.com>
 */
public class SymmetricCryptoContext implements ICryptoContext {
    /** AES encryption cipher algorithm. */
    private static final String AES_ALGO = "AES";
    /** AES encryption cipher algorithm. */
    private static final String AES_TRANSFORM = AES_ALGO + "/CBC/PKCS5Padding";
    /** AES encryption initial value size in bytes. */
    private static final int AES_IV_SIZE = 16;

    /** HMAC SHA-256 algorithm. */
    private static final String HMAC_SHA256_ALGO = "HmacSHA256";

    /** AES key wrap cipher algorithm. */
    private static final String AESKW_ALGO = "AES";
    /** AES key wrap cipher transform. */
    private static final String AESKW_TRANSFORM = AESKW_ALGO + "/ECB/NoPadding";
    /** AES key wrap block size in bytes. */
    private static final int AESKW_BLOCK_SIZE = 8;
    /** Key wrap initial value. */
    private static final byte[] AESKW_AIV = { (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6, (byte) 0xA6,
            (byte) 0xA6, (byte) 0xA6, (byte) 0xA6 };

    /**
     * @param bytes number of bytes to return.
     * @param w the value.
     * @return the specified number of most significant (big-endian) bytes of
     *         the value.
     */
    private static byte[] msb(final int bytes, final byte[] w) {
        final byte[] msb = new byte[bytes];
        System.arraycopy(w, 0, msb, 0, bytes);
        return msb;
    }

    /**
     * @param bytes number of bytes to return.
     * @param w the value.
     * @return the specified number of least significant (big-endian) bytes of
     *         the value.
     */
    private static byte[] lsb(final int bytes, final byte[] w) {
        final int offset = w.length - bytes;
        final byte[] lsb = new byte[bytes];
        for (int i = 0; i < bytes; ++i)
            lsb[i] = w[offset + i];
        return lsb;
    }

    /**
     * Modifies the provided byte array by XOR'ing it with the provided value.
     * The byte array is processed in big-endian order.
     * 
     * @param b 8-byte value that will be modified.
     * @param t the 64-bit value to XOR the value with.
     */
    private static void xor(final byte[] b, final long t) {
        b[0] ^= t >>> 56;
        b[1] ^= t >>> 48;
        b[2] ^= t >>> 40;
        b[3] ^= t >>> 32;
        b[4] ^= t >>> 24;
        b[5] ^= t >>> 16;
        b[6] ^= t >>> 8;
        b[7] ^= t;
    }

    /**
     * <p>Create a new symmetric crypto context using the provided keys.</p>
     * 
     * <p>If there is no encryption key, encryption and decryption is
     * unsupported.</p>
     * 
     * <p>If there is no signature key, signing and verification is
     * unsupported.</p>
     * 
     * <p>If there is no wrapping key, wrap and unwrap is unsupported.</p>
     * 
     * @param ctx MSL context.
     * @param id the key set identity.
     * @param encryptionKey the key used for encryption/decryption.
     * @param signatureKey the key used for HMAC or CMAC computation.
     * @param wrappingKey the key used for wrap/unwrap.     */
    public SymmetricCryptoContext(final MslContext ctx, final String id, final SecretKey encryptionKey,
            final SecretKey signatureKey, final SecretKey wrappingKey) {
        if (encryptionKey != null && !encryptionKey.getAlgorithm().equals(JcaAlgorithm.AES))
            throw new IllegalArgumentException("Encryption key must be an " + JcaAlgorithm.AES + " key.");
        if (signatureKey != null && !signatureKey.getAlgorithm().equals(JcaAlgorithm.HMAC_SHA256)
                && !signatureKey.getAlgorithm().equals(JcaAlgorithm.AES_CMAC)) {
            throw new IllegalArgumentException("Encryption key must be an " + JcaAlgorithm.HMAC_SHA256 + " or "
                    + JcaAlgorithm.AES_CMAC + " key.");
        }
        if (wrappingKey != null && !wrappingKey.getAlgorithm().equals(JcaAlgorithm.AESKW))
            throw new IllegalArgumentException("Encryption key must be an " + JcaAlgorithm.AESKW + " key.");

        this.ctx = ctx;
        this.id = id;
        this.encryptionKey = encryptionKey;
        this.signatureKey = signatureKey;
        this.wrappingKey = wrappingKey;
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.MslCryptoContext#encrypt(byte[])
     */
    @Override
    public byte[] encrypt(final byte[] data) throws MslCryptoException {
        if (encryptionKey == null)
            throw new MslCryptoException(MslError.ENCRYPT_NOT_SUPPORTED, "no encryption/decryption key");
        try {
            // Generate IV.
            final Random random = ctx.getRandom();
            final byte[] iv = new byte[AES_IV_SIZE];
            random.nextBytes(iv);

            // Encrypt plaintext.
            final byte[] ciphertext;
            if (data.length != 0) {
                final Cipher cipher = CryptoCache.getCipher(AES_TRANSFORM);
                final AlgorithmParameterSpec params = new IvParameterSpec(iv);
                cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, params);
                ciphertext = cipher.doFinal(data);
            } else {
                ciphertext = new byte[0];
            }

            // Return encryption envelope byte representation.
            return new MslCiphertextEnvelope(id, iv, ciphertext).toJSONString()
                    .getBytes(MslConstants.DEFAULT_CHARSET);
        } 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_ENCRYPTION_KEY, e);
        } catch (final InvalidAlgorithmParameterException e) {
            throw new MslCryptoException(MslError.INVALID_ALGORITHM_PARAMS, e);
        } catch (final IllegalBlockSizeException e) {
            throw new MslCryptoException(MslError.PLAINTEXT_ILLEGAL_BLOCK_SIZE,
                    "not expected when padding is specified", e);
        } catch (final BadPaddingException e) {
            throw new MslCryptoException(MslError.PLAINTEXT_BAD_PADDING, "not expected when encrypting", e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.MslCryptoContext#decrypt(byte[])
     */
    @Override
    public byte[] decrypt(final byte[] data) throws MslCryptoException {
        if (encryptionKey == null)
            throw new MslCryptoException(MslError.DECRYPT_NOT_SUPPORTED, "no encryption/decryption key");
        try {
            // Reconstitute encryption envelope.
            final JSONObject encryptionEnvelopeJsonObj = new JSONObject(
                    new String(data, MslConstants.DEFAULT_CHARSET));
            final MslCiphertextEnvelope encryptionEnvelope = new MslCiphertextEnvelope(encryptionEnvelopeJsonObj,
                    MslCiphertextEnvelope.Version.V1);

            // Verify key ID.
            if (!encryptionEnvelope.getKeyId().equals(id))
                throw new MslCryptoException(MslError.ENVELOPE_KEY_ID_MISMATCH);

            // Decrypt ciphertext.
            final byte[] ciphertext = encryptionEnvelope.getCiphertext();
            if (ciphertext.length == 0)
                return new byte[0];
            final byte[] iv = encryptionEnvelope.getIv();
            final Cipher cipher = CryptoCache.getCipher(AES_TRANSFORM);
            final AlgorithmParameterSpec params = new IvParameterSpec(iv);
            cipher.init(Cipher.DECRYPT_MODE, encryptionKey, params);
            return cipher.doFinal(ciphertext);
        } catch (final ArrayIndexOutOfBoundsException e) {
            throw new MslCryptoException(MslError.INSUFFICIENT_CIPHERTEXT, e);
        } catch (final JSONException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_ENVELOPE_PARSE_ERROR, e);
        } catch (final MslEncodingException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_ENVELOPE_PARSE_ERROR, e);
        } catch (final NoSuchAlgorithmException e) {
            throw new MslInternalException("Invalid cipher algorithm specified.", e);
        } catch (NoSuchPaddingException e) {
            throw new MslInternalException("Unsupported padding exception.", e);
        } catch (final InvalidKeyException e) {
            throw new MslCryptoException(MslError.INVALID_ENCRYPTION_KEY, e);
        } catch (final InvalidAlgorithmParameterException e) {
            throw new MslCryptoException(MslError.INVALID_ALGORITHM_PARAMS, e);
        } catch (final IllegalBlockSizeException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_ILLEGAL_BLOCK_SIZE, e);
        } catch (final BadPaddingException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_BAD_PADDING, e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#wrap(byte[])
     */
    @Override
    public byte[] wrap(final byte[] data) throws MslCryptoException {
        if (wrappingKey == null)
            throw new MslCryptoException(MslError.WRAP_NOT_SUPPORTED, "no wrap/unwrap key");
        if (data.length % 8 != 0)
            throw new MslCryptoException(MslError.PLAINTEXT_ILLEGAL_BLOCK_SIZE, "data.length " + data.length);

        // Compute alternate initial value.
        byte[] a = AESKW_AIV.clone();
        final byte[] r = data.clone();
        try {
            final Cipher cipher = CryptoCache.getCipher(AESKW_TRANSFORM);
            cipher.init(Cipher.ENCRYPT_MODE, wrappingKey);

            // Initialize variables.
            final int n = r.length / AESKW_BLOCK_SIZE;

            // Calculate intermediate values.
            for (int j = 0; j < 6; ++j) {
                for (int i = 1; i <= n; ++i) {
                    byte[] r_i = Arrays.copyOfRange(r, (i - 1) * AESKW_BLOCK_SIZE, i * AESKW_BLOCK_SIZE);
                    final byte[] ar_i = Arrays.copyOf(a, a.length + r_i.length);
                    System.arraycopy(r_i, 0, ar_i, a.length, r_i.length);
                    final byte[] b = cipher.doFinal(ar_i);
                    a = msb(AESKW_BLOCK_SIZE, b);
                    final long t = (n * j) + i;
                    xor(a, t);
                    r_i = lsb(AESKW_BLOCK_SIZE, b);
                    System.arraycopy(r_i, 0, r, (i - 1) * AESKW_BLOCK_SIZE, AESKW_BLOCK_SIZE);
                }
            }

            // Output results.
            final byte[] c = new byte[a.length + r.length];
            System.arraycopy(a, 0, c, 0, a.length);
            System.arraycopy(r, 0, c, a.length, r.length);
            return c;
        } catch (final NoSuchAlgorithmException e) {
            throw new MslInternalException("Invalid cipher algorithm specified.", e);
        } catch (final NoSuchPaddingException e) {
            throw new MslInternalException("Unsupported padding exception.", e);
        } catch (final InvalidKeyException e) {
            throw new MslCryptoException(MslError.INVALID_WRAPPING_KEY, e);
        } catch (final IllegalBlockSizeException e) {
            throw new MslCryptoException(MslError.PLAINTEXT_ILLEGAL_BLOCK_SIZE,
                    "not expected when padding is no padding", e);
        } catch (final BadPaddingException e) {
            throw new MslCryptoException(MslError.PLAINTEXT_BAD_PADDING, "not expected when encrypting", e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.ICryptoContext#unwrap(byte[])
     */
    @Override
    public byte[] unwrap(final byte[] data) throws MslCryptoException {
        if (wrappingKey == null)
            throw new MslCryptoException(MslError.UNWRAP_NOT_SUPPORTED, "no wrap/unwrap key");
        if (data.length % 8 != 0)
            throw new MslCryptoException(MslError.CIPHERTEXT_ILLEGAL_BLOCK_SIZE, "data.length " + data.length);

        try {
            final Cipher cipher = CryptoCache.getCipher(AESKW_TRANSFORM);
            cipher.init(Cipher.DECRYPT_MODE, wrappingKey);

            byte[] a = Arrays.copyOf(data, AESKW_BLOCK_SIZE);
            final byte[] r = Arrays.copyOfRange(data, a.length, data.length);
            final int n = (data.length - AESKW_BLOCK_SIZE) / AESKW_BLOCK_SIZE;

            // Calculate intermediate values.
            for (int j = 5; j >= 0; --j) {
                for (int i = n; i >= 1; --i) {
                    final long t = (n * j) + i;
                    xor(a, t);
                    byte[] r_i = Arrays.copyOfRange(r, (i - 1) * AESKW_BLOCK_SIZE, i * AESKW_BLOCK_SIZE);
                    final byte[] ar_i = Arrays.copyOf(a, a.length + r_i.length);
                    System.arraycopy(r_i, 0, ar_i, a.length, r_i.length);
                    final byte[] b = cipher.doFinal(ar_i);
                    a = msb(AESKW_BLOCK_SIZE, b);
                    r_i = lsb(AESKW_BLOCK_SIZE, b);
                    System.arraycopy(r_i, 0, r, (i - 1) * AESKW_BLOCK_SIZE, AESKW_BLOCK_SIZE);
                }
            }

            // Output results.
            if (MslUtils.safeEquals(a, AESKW_AIV) && r.length % AESKW_BLOCK_SIZE == 0)
                return r;
            throw new MslCryptoException(MslError.UNWRAP_ERROR, "initial value " + Arrays.toString(a));
        } 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_WRAPPING_KEY, e);
        } catch (final IllegalBlockSizeException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_ILLEGAL_BLOCK_SIZE, e);
        } catch (final BadPaddingException e) {
            throw new MslCryptoException(MslError.CIPHERTEXT_BAD_PADDING, e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.MslCryptoContext#sign(byte[])
     */
    @Override
    public byte[] sign(final byte[] data) throws MslCryptoException {
        if (signatureKey == null)
            throw new MslCryptoException(MslError.SIGN_NOT_SUPPORTED, "No signature key.");
        try {
            // Compute the xMac.
            final byte[] xmac;
            if (signatureKey.getAlgorithm().equals(JcaAlgorithm.HMAC_SHA256)) {
                final Mac mac = CryptoCache.getMac(HMAC_SHA256_ALGO);
                mac.init(signatureKey);
                xmac = mac.doFinal(data);
            } else if (signatureKey.getAlgorithm().equals(JcaAlgorithm.AES_CMAC)) {
                final CipherParameters params = new KeyParameter(signatureKey.getEncoded());
                final BlockCipher aes = new AESEngine();
                final CMac mac = new CMac(aes);
                mac.init(params);
                mac.update(data, 0, data.length);
                xmac = new byte[mac.getMacSize()];
                mac.doFinal(xmac, 0);
            } else {
                throw new MslCryptoException(MslError.SIGN_NOT_SUPPORTED, "Unsupported algorithm.");
            }

            // Return the signature envelope byte representation.
            return new MslSignatureEnvelope(xmac).getBytes();
        } catch (final NoSuchAlgorithmException e) {
            throw new MslInternalException("Invalid MAC algorithm specified.", e);
        } catch (final InvalidKeyException e) {
            throw new MslCryptoException(MslError.INVALID_HMAC_KEY, e);
        }
    }

    /* (non-Javadoc)
     * @see com.netflix.msl.crypto.MslCryptoContext#verify(byte[], byte[])
     */
    @Override
    public boolean verify(final byte[] data, final byte[] signature) throws MslCryptoException {
        if (signatureKey == null)
            throw new MslCryptoException(MslError.VERIFY_NOT_SUPPORTED, "No signature key.");
        try {
            // Reconstitute the signature envelope.
            final MslSignatureEnvelope envelope = MslSignatureEnvelope.parse(signature);

            // Compute the xMac.
            final byte[] xmac;
            if (signatureKey.getAlgorithm().equals(JcaAlgorithm.HMAC_SHA256)) {
                final Mac mac = CryptoCache.getMac(HMAC_SHA256_ALGO);
                mac.init(signatureKey);
                xmac = mac.doFinal(data);
            } else if (signatureKey.getAlgorithm().equals(JcaAlgorithm.AES_CMAC)) {
                final CipherParameters params = new KeyParameter(signatureKey.getEncoded());
                final BlockCipher aes = new AESEngine();
                final CMac mac = new CMac(aes);
                mac.init(params);
                mac.update(data, 0, data.length);
                xmac = new byte[mac.getMacSize()];
                mac.doFinal(xmac, 0);
            } else {
                throw new MslCryptoException(MslError.VERIFY_NOT_SUPPORTED, "Unsupported algorithm.");
            }

            // Compare the computed hash to the provided signature.
            return MslUtils.safeEquals(xmac, envelope.getSignature());
        } catch (final MslEncodingException e) {
            throw new MslCryptoException(MslError.SIGNATURE_ENVELOPE_PARSE_ERROR, e);
        } catch (final NoSuchAlgorithmException e) {
            throw new MslInternalException("Invalid MAC algorithm specified.", e);
        } catch (final InvalidKeyException e) {
            throw new MslCryptoException(MslError.INVALID_HMAC_KEY, e);
        }
    }

    /** MSL context. */
    protected final MslContext ctx;
    /** Key set identity. */
    protected final String id;
    /** Encryption/decryption key. */
    protected final SecretKey encryptionKey;
    /** Signature key. */
    protected final SecretKey signatureKey;
    /** Wrapping key. */
    protected final SecretKey wrappingKey;
}