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

io.jsonwebtoken.impl.security.AesAlgorithm 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.lang.Bytes;
import io.jsonwebtoken.lang.Arrays;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.IvSupplier;
import io.jsonwebtoken.security.KeyBuilderSupplier;
import io.jsonwebtoken.security.KeyLengthSupplier;
import io.jsonwebtoken.security.Request;
import io.jsonwebtoken.security.SecretKeyBuilder;
import io.jsonwebtoken.security.WeakKeyException;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

/**
 * @since 0.12.0
 */
abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplier, KeyLengthSupplier {

    protected static final String KEY_ALG_NAME = "AES";
    protected static final int BLOCK_SIZE = 128;
    protected static final int BLOCK_BYTE_SIZE = BLOCK_SIZE / Byte.SIZE;
    protected static final int GCM_IV_SIZE = 96; // https://tools.ietf.org/html/rfc7518#section-5.3
    //protected static final int GCM_IV_BYTE_SIZE = GCM_IV_SIZE / Byte.SIZE;
    protected static final String DECRYPT_NO_IV = "This algorithm implementation rejects decryption " +
            "requests that do not include initialization vectors. AES ciphertext without an IV is weak and " +
            "susceptible to attack.";

    protected final int keyBitLength;
    protected final int ivBitLength;
    protected final int tagBitLength;
    protected final boolean gcm;

    /**
     * Ensures {@code keyBitLength is a valid AES key length}
     * @param keyBitLength the key length (in bits) to check
     * @since 0.12.4
     */
    static void assertKeyBitLength(int keyBitLength) {
        if (keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256) return; // valid
        String msg = "Invalid AES key length: " + Bytes.bitsMsg(keyBitLength) + ". AES only supports " +
                "128, 192, or 256 bit keys.";
        throw new IllegalArgumentException(msg);
    }

    static SecretKey keyFor(byte[] bytes) {
        int bitlen = (int) Bytes.bitLength(bytes);
        assertKeyBitLength(bitlen);
        return new SecretKeySpec(bytes, KEY_ALG_NAME);
    }

    AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) {
        super(id, jcaTransformation);
        assertKeyBitLength(keyBitLength);
        this.keyBitLength = keyBitLength;
        this.gcm = jcaTransformation.startsWith("AES/GCM");
        this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
        // https://tools.ietf.org/html/rfc7518#section-5.2.3 through https://tools.ietf.org/html/rfc7518#section-5.3 :
        this.tagBitLength = this.gcm ? BLOCK_SIZE : this.keyBitLength;
    }

    @Override
    public int getKeyBitLength() {
        return this.keyBitLength;
    }

    @Override
    public SecretKeyBuilder key() {
        return new DefaultSecretKeyBuilder(KEY_ALG_NAME, getKeyBitLength());
    }

    protected SecretKey assertKey(SecretKey key) {
        Assert.notNull(key, "Request key cannot be null.");
        validateLengthIfPossible(key);
        return key;
    }

    private void validateLengthIfPossible(SecretKey key) {
        validateLength(key, this.keyBitLength, false);
    }

    protected static String lengthMsg(String id, String type, int requiredLengthInBits, long actualLengthInBits) {
        return "The '" + id + "' algorithm requires " + type + " with a length of " +
                Bytes.bitsMsg(requiredLengthInBits) + ".  The provided key has a length of " +
                Bytes.bitsMsg(actualLengthInBits) + ".";
    }

    protected byte[] validateLength(SecretKey key, int requiredBitLength, boolean propagate) {
        byte[] keyBytes;

        try {
            keyBytes = key.getEncoded();
        } catch (RuntimeException re) {
            if (propagate) {
                throw re;
            }
            //can't get the bytes to validate, e.g. hardware security module or later Android, so just return:
            return null;
        }
        long keyBitLength = Bytes.bitLength(keyBytes);
        if (keyBitLength < requiredBitLength) {
            throw new WeakKeyException(lengthMsg(getId(), "keys", requiredBitLength, keyBitLength));
        }

        return keyBytes;
    }

    protected byte[] assertBytes(byte[] bytes, String type, int requiredBitLen) {
        long bitLen = Bytes.bitLength(bytes);
        if (requiredBitLen != bitLen) {
            String msg = lengthMsg(getId(), type, requiredBitLen, bitLen);
            throw new IllegalArgumentException(msg);
        }
        return bytes;
    }

    byte[] assertIvLength(final byte[] iv) {
        return assertBytes(iv, "initialization vectors", this.ivBitLength);
    }

    byte[] assertTag(byte[] tag) {
        return assertBytes(tag, "authentication tags", this.tagBitLength);
    }

    byte[] assertDecryptionIv(IvSupplier src) throws IllegalArgumentException {
        byte[] iv = src.getIv();
        Assert.notEmpty(iv, DECRYPT_NO_IV);
        return assertIvLength(iv);
    }

    protected byte[] ensureInitializationVector(Request request) {
        byte[] iv = null;
        if (request instanceof IvSupplier) {
            iv = Arrays.clean(((IvSupplier) request).getIv());
        }
        int ivByteLength = this.ivBitLength / Byte.SIZE;
        if (iv == null || iv.length == 0) {
            iv = new byte[ivByteLength];
            SecureRandom random = ensureSecureRandom(request);
            random.nextBytes(iv);
        } else {
            assertIvLength(iv);
        }
        return iv;
    }

    protected AlgorithmParameterSpec getIvSpec(byte[] iv) {
        Assert.notEmpty(iv, "Initialization Vector byte array cannot be null or empty.");
        return this.gcm ? new GCMParameterSpec(BLOCK_SIZE, iv) : new IvParameterSpec(iv);
    }

    protected void withCipher(Cipher cipher, InputStream in, OutputStream out) throws Exception {
        byte[] last = withCipher(cipher, in, null, out);
        out.write(last); // no AAD, so no tag, so we can just append
    }

    private void updateAAD(Cipher cipher, InputStream aad) throws Exception {
        if (aad == null) return;
        byte[] buf = new byte[2048];
        int len = 0;
        while (len != -1) {
            len = aad.read(buf);
            if (len > 0) {
                cipher.updateAAD(buf, 0, len);
            }
        }
    }

    protected byte[] withCipher(Cipher cipher, InputStream in, InputStream aad, OutputStream out) throws Exception {
        updateAAD(cipher, aad); // no-op if aad is null
        byte[] buf = new byte[2048];
        try {
            int len = 0;
            while (len != -1) {
                len = in.read(buf);
                if (len > 0) {
                    byte[] enc = cipher.update(buf, 0, len);
                    Streams.write(out, enc, "Unable to write Cipher output to OutputStream");
                }
            }
            return cipher.doFinal();
        } finally {
            Bytes.clear(buf);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy