All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.jsonwebtoken.impl.security.HmacAesAeadAlgorithm Maven / Gradle / Ivy

/*
 * Copyright (C) 2021 jsonwebtoken.io
 *
 * 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 io.jsonwebtoken.impl.security;

import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.io.TeeOutputStream;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.AeadAlgorithm;
import io.jsonwebtoken.security.AeadRequest;
import io.jsonwebtoken.security.AeadResult;
import io.jsonwebtoken.security.DecryptAeadRequest;
import io.jsonwebtoken.security.SecretKeyBuilder;
import io.jsonwebtoken.security.SecureRequest;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.security.MessageDigest;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

/**
 * @since 0.12.0
 */
public class HmacAesAeadAlgorithm extends AesAlgorithm implements AeadAlgorithm {

    private static final String TRANSFORMATION_STRING = "AES/CBC/PKCS5Padding";

    private final DefaultMacAlgorithm SIGALG;

    private static int digestLength(int keyLength) {
        return keyLength * 2;
    }

    private static String id(int keyLength) {
        return "A" + keyLength + "CBC-HS" + digestLength(keyLength);
    }

    public HmacAesAeadAlgorithm(String id, DefaultMacAlgorithm sigAlg) {
        super(id, TRANSFORMATION_STRING, sigAlg.getKeyBitLength());
        this.SIGALG = sigAlg;
    }

    public HmacAesAeadAlgorithm(int keyBitLength) {
        this(id(keyBitLength), new DefaultMacAlgorithm(id(keyBitLength), "HmacSHA" + digestLength(keyBitLength), keyBitLength));
    }

    @Override
    public int getKeyBitLength() {
        return super.getKeyBitLength() * 2;
    }

    @Override
    public SecretKeyBuilder key() {
        // The Sun JCE KeyGenerator throws an exception if bitLengths are not standard AES 128, 192 or 256 values.
        // Since the JWA HmacAes algorithms require double that, we use secure-random keys instead:
        return new RandomSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength());
    }

    byte[] assertKeyBytes(SecureRequest request) {
        SecretKey key = Assert.notNull(request.getKey(), "Request key cannot be null.");
        return validateLength(key, this.keyBitLength * 2, true);
    }

    @Override
    public void encrypt(final AeadRequest req, final AeadResult res) {

        Assert.notNull(req, "Request cannot be null.");
        Assert.notNull(res, "Result cannot be null.");

        byte[] compositeKeyBytes = assertKeyBytes(req);
        int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2
        byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount);
        byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length);
        final SecretKey encryptionKey;
        try {
            encryptionKey = new SecretKeySpec(encKeyBytes, KEY_ALG_NAME);
        } finally {
            Bytes.clear(encKeyBytes);
            Bytes.clear(compositeKeyBytes);
        }

        final InputStream plaintext = Assert.notNull(req.getPayload(),
                "Request content (plaintext) InputStream cannot be null.");
        final OutputStream out = Assert.notNull(res.getOutputStream(), "Result ciphertext OutputStream cannot be null.");
        final InputStream aad = req.getAssociatedData(); //can be null if there's no associated data
        final byte[] iv = ensureInitializationVector(req);
        final AlgorithmParameterSpec ivSpec = getIvSpec(iv);

        // we need the ciphertext bytes for message digest calculation, so we'll use a TeeOutputStream to
        // aggregate those results while they're being written to the result output stream
        final ByteArrayOutputStream copy = new ByteArrayOutputStream(8192);
        final OutputStream tee = new TeeOutputStream(out, copy);

        jca(req).withCipher(new CheckedFunction() {
            @Override
            public Object apply(Cipher cipher) throws Exception {
                cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, ivSpec);
                withCipher(cipher, plaintext, tee);
                return null; // don't need to return anything
            }
        });

        byte[] aadBytes = aad == null ? Bytes.EMPTY : Streams.bytes(aad, "Unable to read AAD bytes.");

        byte[] tag;
        try {
            tag = sign(aadBytes, iv, Streams.of(copy.toByteArray()), macKeyBytes);
            res.setTag(tag).setIv(iv);
        } finally {
            Bytes.clear(macKeyBytes);
        }
    }

    private byte[] sign(byte[] aad, byte[] iv, InputStream ciphertext, byte[] macKeyBytes) {

        long aadLength = io.jsonwebtoken.lang.Arrays.length(aad);
        long aadLengthInBits = aadLength * Byte.SIZE;
        long aadLengthInBitsAsUnsignedInt = aadLengthInBits & 0xffffffffL;
        byte[] AL = Bytes.toBytes(aadLengthInBitsAsUnsignedInt);

        Collection streams = new ArrayList<>(4);
        if (!Bytes.isEmpty(aad)) { // must come first if it exists
            streams.add(Streams.of(aad));
        }
        streams.add(Streams.of(iv));
        streams.add(ciphertext);
        streams.add(Streams.of(AL));
        InputStream in = new SequenceInputStream(Collections.enumeration(streams));

        SecretKey key = new SecretKeySpec(macKeyBytes, SIGALG.getJcaName());
        SecureRequest request =
                new DefaultSecureRequest<>(in, null, null, key);
        byte[] digest = SIGALG.digest(request);

        // https://tools.ietf.org/html/rfc7518#section-5.2.2.1 #5 requires truncating the signature
        // to be the same length as the macKey/encKey:
        return assertTag(Arrays.copyOfRange(digest, 0, macKeyBytes.length));
    }

    @Override
    public void decrypt(final DecryptAeadRequest req, final OutputStream plaintext) {

        Assert.notNull(req, "Request cannot be null.");
        Assert.notNull(plaintext, "Plaintext OutputStream cannot be null.");

        byte[] compositeKeyBytes = assertKeyBytes(req);
        int halfCount = compositeKeyBytes.length / 2; // https://tools.ietf.org/html/rfc7518#section-5.2
        byte[] macKeyBytes = Arrays.copyOfRange(compositeKeyBytes, 0, halfCount);
        byte[] encKeyBytes = Arrays.copyOfRange(compositeKeyBytes, halfCount, compositeKeyBytes.length);
        final SecretKey decryptionKey;
        try {
            decryptionKey = new SecretKeySpec(encKeyBytes, KEY_ALG_NAME);
        } finally {
            Bytes.clear(encKeyBytes);
            Bytes.clear(compositeKeyBytes);
        }

        InputStream in = Assert.notNull(req.getPayload(),
                "Decryption request content (ciphertext) InputStream cannot be null.");
        final InputStream aad = req.getAssociatedData(); // can be null if there's no associated data
        final byte[] tag = assertTag(req.getDigest());
        final byte[] iv = assertDecryptionIv(req);
        final AlgorithmParameterSpec ivSpec = getIvSpec(iv);

        // Assert that the aad + iv + ciphertext provided, when signed, equals the tag provided,
        // thereby verifying none of it has been tampered with:
        byte[] aadBytes = aad == null ? Bytes.EMPTY : Streams.bytes(aad, "Unable to read AAD bytes.");
        byte[] digest;
        try {
            digest = sign(aadBytes, iv, in, macKeyBytes);
        } finally {
            Bytes.clear(macKeyBytes);
        }
        if (!MessageDigest.isEqual(digest, tag)) { //constant time comparison to avoid side-channel attacks
            String msg = "Ciphertext decryption failed: Authentication tag verification failed.";
            throw new SignatureException(msg);
        }
        Streams.reset(in); // rewind for decryption

        final InputStream ciphertext = in;
        jca(req).withCipher(new CheckedFunction() {
            @Override
            public byte[] apply(Cipher cipher) throws Exception {
                cipher.init(Cipher.DECRYPT_MODE, decryptionKey, ivSpec);
                withCipher(cipher, ciphertext, plaintext);
                return Bytes.EMPTY;
            }
        });
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy