AESCBCHMACSHA2ContentEncryptionHandler.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */

package org.forgerock.json.jose.jwe.handlers.encryption;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.forgerock.json.jose.exceptions.JweDecryptionException;
import org.forgerock.json.jose.exceptions.JweEncryptionException;
import org.forgerock.json.jose.jwe.EncryptionMethod;
import org.forgerock.json.jose.jwe.JweEncryption;
import org.forgerock.json.jose.utils.Utils;

/**
 * Encrypts using AES in CBC mode with PKCS#5 padding and uses a separate HMAC-SHA2 tag to authenticate.
 */
final class AESCBCHMACSHA2ContentEncryptionHandler extends ContentEncryptionHandler {
    private static final Logger LOGGER = Logger.getLogger(AESCBCHMACSHA2ContentEncryptionHandler.class.getName());
    private static final String RAW_KEY_FORMAT = "RAW";
    private final EncryptionMethod method;

    AESCBCHMACSHA2ContentEncryptionHandler(final EncryptionMethod method) {
        this.method = method;
    }

    @Override
    public JweEncryption encrypt(final Key key, final byte[] iv, final byte[] plainText, final byte[] additionalData) {

        final Key macKey = macKey(key, method);
        final Key encryptionKey = encKey(key, method);

        try {
            final Cipher cipher = Cipher.getInstance(method.getTransformation());
            cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
            final byte[] cipherText = cipher.doFinal(plainText);

            long alLength = additionalData.length * 8L;
            byte[] al = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putLong(alLength).array();

            final Mac mac = Mac.getInstance(method.getMacAlgorithm());
            mac.init(macKey);
            mac.update(additionalData);
            mac.update(iv);
            mac.update(cipherText);
            mac.update(al);

            byte[] authenticationTag = Arrays.copyOf(mac.doFinal(), method.getKeyOffset());

            return new JweEncryption(cipherText, authenticationTag);
        } catch (GeneralSecurityException e) {
            throw new JweEncryptionException(e);
        }
    }

    @Override
    public byte[] decrypt(final Key key, final byte[] iv, final JweEncryption cipherText, final byte[] additionalData) {
        final Key macKey = macKey(key, method);
        final Key encKey = encKey(key, method);

        long alLength = additionalData.length * 8L;
        byte[] al = ByteBuffer.allocate(8).order(ByteOrder.BIG_ENDIAN).putLong(alLength).array();

        try {
            final Mac mac = Mac.getInstance(method.getMacAlgorithm());
            mac.init(macKey);
            mac.update(additionalData);
            mac.update(iv);
            mac.update(cipherText.getCiphertext());
            mac.update(al);

            final byte[] tag = Arrays.copyOf(mac.doFinal(), method.getKeyOffset());

            final boolean macValid = Utils.constantEquals(tag, cipherText.getAuthenticationTag());

            final Cipher cipher = Cipher.getInstance(method.getTransformation());
            cipher.init(Cipher.DECRYPT_MODE, encKey, new IvParameterSpec(iv));
            final byte[] plainText = cipher.doFinal(cipherText.getCiphertext());

            if (!macValid) {
                throw new GeneralSecurityException("MAC verification failed");
            }

            return plainText;

        } catch (GeneralSecurityException ex) {
            if (LOGGER.isLoggable(Level.FINE)) {
                LOGGER.log(Level.FINE, "Decryption failed: " + ex, ex);
            }
            throw new JweDecryptionException();
        }
    }

    @Override
    Key generateEncryptionKey() {
        // We need to generate a CEK sufficiently large to supply the key for the AES block cipher and the HMAC. As
        // the HMAC algorithms are all truncated to half of their output size, it is sufficient to generate a CEK
        // with size (AES key size) + (HMAC SHA Key Size / 2). For example, AES128HS256 will produce a CEK of length
        // 128 + (256/2) = 256 bits, while AES256HS512 will use 256 + (512/2) = 512 bits. In the latter case, we
        // cannot simply generate an "AES" key of 512 bits, as this is an invalid key size for AES (and will generate
        // an exception if we try). So instead, we generate separate keys for the MAC and the AES cipher and
        // concatenate them as CEK = MAC_KEY + ENC_KEY as per https://tools.ietf.org/html/rfc7518#section-5.2.2.1

        try {
            // The keyOffset field gives the size of the MAC key in octets
            final int macKeySize = method.getKeyOffset() * 8;
            final KeyGenerator macKeyGenerator = KeyGenerator.getInstance(method.getMacAlgorithm());
            macKeyGenerator.init(macKeySize);
            final Key macKey = macKeyGenerator.generateKey();
            if (!RAW_KEY_FORMAT.equals(macKey.getFormat())) {
                throw new IllegalStateException("HMAC KeyGenerator returned non-RAW key material!");
            }

            final int encKeySize = method.getKeySize() - macKeySize;
            final KeyGenerator encKeyGenerator = KeyGenerator.getInstance(method.getEncryptionAlgorithm());
            encKeyGenerator.init(encKeySize);
            final Key encKey = encKeyGenerator.generateKey();
            if (!RAW_KEY_FORMAT.equals(macKey.getFormat())) {
                throw new IllegalStateException("AES KeyGenerator returned non-RAW key material!");
            }

            final byte[] combinedKey = ByteBuffer.allocate(method.getKeySize() / 8)
                                                 .put(macKey.getEncoded())
                                                 .put(encKey.getEncoded())
                                                 .array();

            return new SecretKeySpec(combinedKey, method.getEncryptionAlgorithm());
        } catch (NoSuchAlgorithmException e) {
            throw new JweEncryptionException("Unsupported Encryption Algorithm, "
                    + method.getEncryptionAlgorithm(), e);
        }

    }

    private static SecretKey macKey(final Key combinedKey, final EncryptionMethod method) {
        return new SecretKeySpec(combinedKey.getEncoded(), 0, method.getKeyOffset(), method.getMacAlgorithm());
    }

    private static SecretKey encKey(final Key combinedKey, final EncryptionMethod method) {
        return new SecretKeySpec(combinedKey.getEncoded(), method.getKeyOffset(), method.getKeyOffset(),
                method.getEncryptionAlgorithm());
    }
}