ConfigCryptoAesGcmEngine.java

package dev.orne.config.impl;

/*-
 * #%L
 * Orne Config
 * %%
 * Copyright (C) 2019 - 2025 Orne Developments
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */

import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.Objects;

import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import javax.validation.constraints.NotNull;

import org.apiguardian.api.API;

import dev.orne.config.ConfigCryptoProviderException;
import dev.orne.config.ConfigCryptoWrongKeyException;

/**
 * Implementation of {@code ConfigCryptoEngine} based on
 * Java Cryptography Architecture using AES with GCM symmetric algorithm.
 * 
 * @author <a href="https://github.com/ihernaez">(w) Iker Hernaez</a>
 * @version 1.0, 2020-08
 * @since 0.2
 */
@API(status = API.Status.INTERNAL, since = "1.0")
public class ConfigCryptoAesGcmEngine
extends AbstractConfigCryptoEngine {

    /** The default {@code SecretKeyFactory} algorithm. */
    public static final String DEFAULT_KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA256";
    /** The default {@code SecretKey} algorithm. */
    public static final String DEFAULT_KEY_ALGORITHM = "AES";
    /** The default {@code Cipher} algorithm. */
    public static final String DEFAULT_CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    /** The default {@code SecretKey} salt iterations. */
    public static final int DEFAULT_SECRET_KEY_ITERATIONS = 65536;
    /** The default {@code SecretKey} length. */
    public static final int DEFAULT_SECRET_KEY_LENGTH = 256;
    /** The default GCM initial vector length. */
    public static final int DEFAULT_GCM_IV_LENGTH = 12;
    /** The default GCM tag length. */
    public static final int DEFAULT_GCM_TAG_LENGTH = 16;

    /** Message for {@code SecretKey} creation errors. */
    private static final String SECRET_KEY_CREATION_ERROR =
            "Error creating secret key";
    /** Message for encryption errors. */
    private static final String ENCRYPTION_ERROR =
            "Error encrypting secret value";
    /** Message for decryption errors. */
    private static final String DECRYPTION_ERROR =
            "Error decrypting secret value";
    /** Message for decryption errors caused by invalid GCM tag validations. */
    private static final String DECRYPTION_WRONG_KEY_ERROR = 
            "Error decrypting secret value. Original value encrypted with another key?";

    /** The {@code SecretKeyFactory} algorithm. */
    private final @NotNull String secretKeyFactoryAlgorithm;
    /** The generated {@code SecretKey} salt iterations. */
    private int secretKeyIterations = DEFAULT_SECRET_KEY_ITERATIONS;
    /** The generated {@code SecretKey} length. */
    private int secretKeyLength = DEFAULT_SECRET_KEY_LENGTH;
    /** The {@code SecretKey} algorithm. */
    private final @NotNull String secretKeyAlgorithm;
    /** The salt used for the {@code SecretKey} creations. */
    private final @NotNull byte[] secretKeySalt;
    /** The {@code Cipher} algorithm. */
    private final @NotNull String cipherAlgorithm;
    /** The GCM initial vector length. */
    private int gcmInitVectorLength = DEFAULT_GCM_IV_LENGTH;
    /** The GCM tag length. */
    private int gcmTagLength = DEFAULT_GCM_TAG_LENGTH;

    /**
     * Creates a new instance with the default {@code SecretKeyFactory},
     * {@code SecretKey} and {@code Cipher} algorithms.
     * 
     * @param secretKeySalt The salt used for the {@code SecretKey} creations.
     */
    public ConfigCryptoAesGcmEngine(
            final @NotNull byte[] secretKeySalt) {
        this(DEFAULT_KEY_FACTORY_ALGORITHM, DEFAULT_KEY_ALGORITHM, secretKeySalt, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * Creates a new instance with the specified {@code SecretKeyFactory},
     * {@code SecretKey} and {@code Cipher} algorithms.
     * 
     * @param secretKeyFactoryAlgorithm The {@code SecretKeyFactory} algorithm
     * @param secretKeyAlgorithm The {@code SecretKey} algorithm
     * @param secretKeySalt The salt used for the {@code SecretKey} creations.
     * @param cipherAlgorithm The {@code Cipher} algorithm
     */
    public ConfigCryptoAesGcmEngine(
            final @NotNull String secretKeyFactoryAlgorithm,
            final @NotNull String secretKeyAlgorithm,
            final @NotNull byte[] secretKeySalt,
            final @NotNull String cipherAlgorithm) {
        super();
        this.secretKeyFactoryAlgorithm = Objects.requireNonNull(secretKeyFactoryAlgorithm);
        this.secretKeyAlgorithm = Objects.requireNonNull(secretKeyAlgorithm);
        this.secretKeySalt = Objects.requireNonNull(secretKeySalt);
        this.cipherAlgorithm = Objects.requireNonNull(cipherAlgorithm);
        if (this.secretKeySalt.length == 0) {
            throw new IllegalArgumentException("Secret key salt cannot be empty");
        }
    }

    /**
     * Returns the {@code SecretKeyFactory} algorithm.
     * 
     * @return The {@code SecretKeyFactory} algorithm.
     */
    public @NotNull String getSecretKeyFactoryAlgorithm() {
        return this.secretKeyFactoryAlgorithm;
    }

    /**
     * Returns the {@code SecretKey} salt iterations.
     * 
     * @return The {@code SecretKey} salt iterations
     */
    public int getSecretKeyIterations() {
        return this.secretKeyIterations;
    }

    /**
     * Sets the generated {@code SecretKey} salt iterations.
     * 
     * @param iterations The generated {@code SecretKey} salt iterations
     */
    public void setSecretKeyIterations(final int iterations) {
        this.secretKeyIterations = iterations;
    }

    /**
     * Returns the generated {@code SecretKey} length.
     * 
     * @return The generated {@code SecretKey} length
     */
    public int getSecretKeyLength() {
        return this.secretKeyLength;
    }

    /**
     * Sets the generated {@code SecretKey} length.
     * 
     * @param length The generated {@code SecretKey} length
     */
    public void setSecretKeyLength(final int length) {
        this.secretKeyLength = length;
    }

    /**
     * Returns the {@code SecretKey} algorithm.
     * 
     * @return The {@code SecretKey} algorithm
     */
    public @NotNull String getSecretKeyAlgorithm() {
        return this.secretKeyAlgorithm;
    }

    /**
     * Returns the {@code Cipher} algorithm.
     * 
     * @return The {@code Cipher} algorithm
     */
    public @NotNull String getCipherAlgorithm() {
        return this.cipherAlgorithm;
    }

    /**
     * Returns the GCM initial vector length.
     * 
     * @return The GCM initial vector length
     */
    public int getGcmInitVectorLength() {
        return this.gcmInitVectorLength;
    }

    /**
     * Sets the GCM initial vector length.
     * 
     * @param length The GCM initial vector length
     */
    public void setGcmInitVectorLength(final int length) {
        this.gcmInitVectorLength = length;
    }

    /**
     * Returns the GCM tag length.
     * 
     * @return The GCM tag length
     */
    public int getGcmTagLength() {
        return this.gcmTagLength;
    }

    /**
     * Sets the GCM tag length.
     * 
     * @param length The GCM tag length
     */
    public void setGcmTagLength(final int length) {
        this.gcmTagLength = length;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull SecretKey createSecretKey(
            final @NotNull char[] password)
    throws ConfigCryptoProviderException {
        checkDestroyed();
        try {
            final SecretKeyFactory factory = getSecretKeyFactory(
                    getSecretKeyFactoryAlgorithm());
            final KeySpec spec = createKeySpec(password);
            final SecretKey tmp = factory.generateSecret(spec);
            return new SecretKeySpec(tmp.getEncoded(),
                    getSecretKeyAlgorithm());
        } catch (final GeneralSecurityException gse) {
            throw new ConfigCryptoProviderException(SECRET_KEY_CREATION_ERROR, gse);
        }
    }

    /**
     * Generates the secret key specification for the specified password
     * and the 
     * 
     * @param password The password
     * @return The secret key specification
     * @throws ConfigCryptoProviderException If an error occurs when creating
     * the specification
     */
    protected @NotNull KeySpec createKeySpec(
            final @NotNull char[] password)
    throws ConfigCryptoProviderException {
        try {
            return new PBEKeySpec(
                    password,
                    this.secretKeySalt,
                    getSecretKeyIterations(),
                    getSecretKeyLength());
        } catch (final IllegalArgumentException iae) {
            throw new ConfigCryptoProviderException(SECRET_KEY_CREATION_ERROR, iae);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull Cipher createCipher()
    throws ConfigCryptoProviderException {
        checkDestroyed();
        return createCipher(getCipherAlgorithm());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull String encrypt(
            final @NotNull String value,
            final @NotNull SecretKey key,
            final @NotNull Cipher cipher)
    throws ConfigCryptoProviderException {
        checkDestroyed();
        final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
        final byte[] initVector = new byte[getGcmInitVectorLength()];
        getSecureRandom().nextBytes(initVector);
        final GCMParameterSpec spec = new GCMParameterSpec(
                getGcmTagLength() * java.lang.Byte.SIZE,
                initVector);
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key, spec, getSecureRandom());
            final int resultBytes = initVector.length + cipher.getOutputSize(valueBytes.length);
            final byte[] ciphertext = new byte[resultBytes];
            System.arraycopy(initVector, 0, ciphertext, 0, initVector.length);
            cipher.doFinal(valueBytes, 0, valueBytes.length, ciphertext, initVector.length);
            return Base64.getEncoder().encodeToString(ciphertext);
        } catch (final GeneralSecurityException gse) {
            throw new ConfigCryptoProviderException(ENCRYPTION_ERROR, gse);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public @NotNull String decrypt(
            final @NotNull String value,
            final @NotNull SecretKey key,
            final @NotNull Cipher cipher)
    throws ConfigCryptoProviderException {
        checkDestroyed();
        final byte[] cipherBytes = Base64.getDecoder().decode(value);
        final GCMParameterSpec gcmSpec = new GCMParameterSpec(
                getGcmTagLength() * java.lang.Byte.SIZE,
                cipherBytes,
                0,
                getGcmInitVectorLength());
        try {
            cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec, getSecureRandom());
            final byte[] valueBytes = cipher.doFinal(
                    cipherBytes,
                    getGcmInitVectorLength(),
                    cipherBytes.length - getGcmInitVectorLength());
            return new String(valueBytes, StandardCharsets.UTF_8);
        } catch (final AEADBadTagException bte) {
            throw new ConfigCryptoWrongKeyException(DECRYPTION_WRONG_KEY_ERROR, bte);
        } catch (final GeneralSecurityException gse) {
            throw new ConfigCryptoProviderException(DECRYPTION_ERROR, gse);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void destroy() {
        Arrays.fill(this.secretKeySalt, (byte) 0);
        super.destroy();
    }
}