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

io.jsonwebtoken.impl.security.Pbes2HsAkwAlgorithm 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.JweHeader;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.DefaultJweHeader;
import io.jsonwebtoken.impl.lang.Bytes;
import io.jsonwebtoken.impl.lang.CheckedFunction;
import io.jsonwebtoken.impl.lang.Parameter;
import io.jsonwebtoken.impl.lang.ParameterReadable;
import io.jsonwebtoken.impl.lang.RequiredParameterReader;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.security.DecryptionKeyRequest;
import io.jsonwebtoken.security.KeyAlgorithm;
import io.jsonwebtoken.security.KeyRequest;
import io.jsonwebtoken.security.KeyResult;
import io.jsonwebtoken.security.Password;
import io.jsonwebtoken.security.SecurityException;

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

/**
 * @since 0.12.0
 */
public class Pbes2HsAkwAlgorithm extends CryptoAlgorithm implements KeyAlgorithm {

    // See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 :
    private static final int DEFAULT_SHA256_ITERATIONS = 310000;
    private static final int DEFAULT_SHA384_ITERATIONS = 210000;
    private static final int DEFAULT_SHA512_ITERATIONS = 120000;

    private static final int MIN_RECOMMENDED_ITERATIONS = 1000; // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2
    private static final String MIN_ITERATIONS_MSG_PREFIX =
            "[JWA RFC 7518, Section 4.8.1.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.2) " +
                    "recommends password-based-encryption iterations be greater than or equal to " +
                    MIN_RECOMMENDED_ITERATIONS + ". Provided: ";
    private static final double MAX_ITERATIONS_FACTOR = 2.5;

    private final int HASH_BYTE_LENGTH;
    private final int DERIVED_KEY_BIT_LENGTH;
    private final byte[] SALT_PREFIX;
    private final int DEFAULT_ITERATIONS;
    private final int MAX_ITERATIONS;
    private final KeyAlgorithm wrapAlg;

    private static byte[] toRfcSaltPrefix(byte[] bytes) {
        // last byte must always be zero as it is a delimiter per
        // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8.1.1
        // We ensure this by creating a byte array that is one element larger than bytes.length since Java defaults all
        // new byte array indices to 0x00, meaning the last one will be our zero delimiter:
        byte[] output = new byte[bytes.length + 1];
        System.arraycopy(bytes, 0, output, 0, bytes.length);
        return output;
    }

    private static int hashBitLength(int keyBitLength) {
        return keyBitLength * 2;
    }

    private static String idFor(int hashBitLength, KeyAlgorithm wrapAlg) {
        Assert.notNull(wrapAlg, "wrapAlg argument cannot be null.");
        return "PBES2-HS" + hashBitLength + "+" + wrapAlg.getId();
    }

    public static int assertIterations(int iterations) {
        if (iterations < MIN_RECOMMENDED_ITERATIONS) {
            String msg = MIN_ITERATIONS_MSG_PREFIX + iterations;
            throw new IllegalArgumentException(msg);
        }
        return iterations;
    }

    public Pbes2HsAkwAlgorithm(int keyBitLength) {
        this(hashBitLength(keyBitLength), new AesWrapKeyAlgorithm(keyBitLength));
    }

    protected Pbes2HsAkwAlgorithm(int hashBitLength, KeyAlgorithm wrapAlg) {
        super(idFor(hashBitLength, wrapAlg), "PBKDF2WithHmacSHA" + hashBitLength);
        this.wrapAlg = wrapAlg; // no need to assert non-null due to 'idFor' implementation above

        // There's some white box knowledge here: there is no need to assert the value of hashBitLength
        // because that is done implicitly in the constructor when instantiating AesWrapKeyAlgorithm. See that class's
        // implementation to see the assertion:
        this.HASH_BYTE_LENGTH = hashBitLength / Byte.SIZE;

        // If the JwtBuilder caller doesn't specify an iteration count, fall back to OWASP best-practice recommendations
        // per https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
        if (hashBitLength >= 512) {
            DEFAULT_ITERATIONS = DEFAULT_SHA512_ITERATIONS;
        } else if (hashBitLength >= 384) {
            DEFAULT_ITERATIONS = DEFAULT_SHA384_ITERATIONS;
        } else {
            DEFAULT_ITERATIONS = DEFAULT_SHA256_ITERATIONS;
        }
        MAX_ITERATIONS = (int) (DEFAULT_ITERATIONS * MAX_ITERATIONS_FACTOR); // upper bound to help mitigate DoS attacks

        // https://www.rfc-editor.org/rfc/rfc7518.html#section-4.8, 2nd paragraph, last sentence:
        // "Their derived-key lengths respectively are 16, 24, and 32 octets." :
        this.DERIVED_KEY_BIT_LENGTH = hashBitLength / 2; // results in 128, 192, or 256

        this.SALT_PREFIX = toRfcSaltPrefix(getId().getBytes(StandardCharsets.UTF_8));
    }

    // protected visibility for testing
    protected SecretKey deriveKey(SecretKeyFactory factory, final char[] password, final byte[] rfcSalt, int iterations) throws Exception {
        PBEKeySpec spec = new PBEKeySpec(password, rfcSalt, iterations, DERIVED_KEY_BIT_LENGTH);
        try {
            SecretKey derived = factory.generateSecret(spec);
            return new SecretKeySpec(derived.getEncoded(), AesAlgorithm.KEY_ALG_NAME); // needed to keep the Sun Provider happy
        } finally {
            spec.clearPassword();
        }
    }

    private SecretKey deriveKey(final KeyRequest request, final char[] password, final byte[] salt, final int iterations) {
        try {
            Assert.notEmpty(password, "Key password character array cannot be null or empty.");
            return jca(request).withSecretKeyFactory(new CheckedFunction() {
                @Override
                public SecretKey apply(SecretKeyFactory factory) throws Exception {
                    return deriveKey(factory, password, salt, iterations);
                }
            });
        } finally {
            java.util.Arrays.fill(password, '\u0000');
        }
    }

    protected byte[] generateInputSalt(KeyRequest request) {
        byte[] inputSalt = new byte[this.HASH_BYTE_LENGTH];
        ensureSecureRandom(request).nextBytes(inputSalt);
        return inputSalt;
    }

    // protected visibility for testing
    protected byte[] toRfcSalt(byte[] inputSalt) {
        return Bytes.concat(this.SALT_PREFIX, inputSalt);
    }

    @Override
    public KeyResult getEncryptionKey(final KeyRequest request) throws SecurityException {

        Assert.notNull(request, "request cannot be null.");
        final Password key = Assert.notNull(request.getPayload(), "Encryption Password cannot be null.");
        final JweHeader header = Assert.notNull(request.getHeader(), "JweHeader cannot be null.");
        Integer p2c = header.getPbes2Count();
        if (p2c == null) { // set a default, and ensure it's available in the header for later decryption:
            p2c = DEFAULT_ITERATIONS;
            header.put(DefaultJweHeader.P2C.getId(), p2c);
        }
        final int iterations = assertIterations(p2c);
        byte[] inputSalt = generateInputSalt(request);
        final byte[] rfcSalt = toRfcSalt(inputSalt);
        char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next:
        final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);

        // now get a new CEK that is encrypted ('wrapped') with the PBE-derived key:
        KeyRequest wrapReq = new DefaultKeyRequest<>(derivedKek, request.getProvider(),
                request.getSecureRandom(), request.getHeader(), request.getEncryptionAlgorithm());
        KeyResult result = wrapAlg.getEncryptionKey(wrapReq);

        request.getHeader().put(DefaultJweHeader.P2S.getId(), inputSalt); //retain for recipients

        return result;
    }

    @Override
    public SecretKey getDecryptionKey(DecryptionKeyRequest request) throws SecurityException {

        JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null.");
        final Password key = Assert.notNull(request.getKey(), "Decryption Password cannot be null.");
        ParameterReadable reader = new RequiredParameterReader(header);
        final byte[] inputSalt = reader.get(DefaultJweHeader.P2S);

        Parameter param = DefaultJweHeader.P2C;
        final int iterations = reader.get(param);
        if (iterations > MAX_ITERATIONS) {
            String msg = "JWE Header " + param + " value " + iterations + " exceeds " + getId() + " maximum " +
                    "allowed value " + MAX_ITERATIONS + ". The larger value is rejected to help mitigate " +
                    "potential Denial of Service attacks.";
            throw new UnsupportedJwtException(msg);
        }

        final byte[] rfcSalt = Bytes.concat(SALT_PREFIX, inputSalt);
        final char[] password = key.toCharArray(); // password will be safely cleaned/zeroed in deriveKey next:
        final SecretKey derivedKek = deriveKey(request, password, rfcSalt, iterations);

        DecryptionKeyRequest unwrapReq =
                new DefaultDecryptionKeyRequest<>(request.getPayload(), request.getProvider(),
                        request.getSecureRandom(), header, request.getEncryptionAlgorithm(), derivedKek);

        return wrapAlg.getDecryptionKey(unwrapReq);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy