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

io.helidon.config.encryption.EncryptionUtil Maven / Gradle / Ivy

Go to download

Configuration filter checking property values and decrypting them if needed. Also provides tools to encrypt values to store them in config files.

The newest version!
/*
 * Copyright (c) 2018, 2023 Oracle and/or its affiliates.
 *
 * 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.helidon.config.encryption;

import java.lang.System.Logger.Level;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;
import java.util.Objects;
import java.util.Optional;

import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import io.helidon.common.Base64Value;
import io.helidon.common.LazyValue;
import io.helidon.common.configurable.Resource;
import io.helidon.common.crypto.AsymmetricCipher;
import io.helidon.common.crypto.PasswordKeyDerivation;
import io.helidon.common.crypto.SymmetricCipher;
import io.helidon.common.pki.Keys;
import io.helidon.config.Config;
import io.helidon.config.ConfigValue;
import io.helidon.config.mp.MpConfig;

/**
 * Encryption utilities for secrets protection.
 */
public final class EncryptionUtil {
    private static final System.Logger LOGGER = System.getLogger(EncryptionUtil.class.getName());

    // SecureRandom instances cannot be in memory when building native image
    private static final LazyValue SECURE_RANDOM = LazyValue.create(SecureRandom::new);

    private static final int SALT_LENGTH = 16;
    private static final int NONCE_LENGTH = 12; //(Also called IV) Needs to be 12 when using GCM!
    private static final int SEED_LENGTH = 16;
    private static final int HASH_ITERATIONS = 10000;
    private static final int KEY_LENGTH_LEGACY = 128;
    private static final int KEY_LENGTH = 256;

    private EncryptionUtil() {
        throw new IllegalStateException("Utility class");
    }

    /**
     * Decrypt using RSA with OAEP.
     * Expects message encrypted with the public key.
     *
     * @param key             private key used to decrypt
     * @param encryptedBase64 base64 encoded encrypted secret
     * @return Secret value
     * @throws ConfigEncryptionException If any problem with decryption occurs
     */
    public static String decryptRsa(PrivateKey key, String encryptedBase64) throws ConfigEncryptionException {
        Objects.requireNonNull(key, "Key must be provided for decryption");
        Objects.requireNonNull(encryptedBase64, "Encrypted bytes must be provided for decryption (base64 encoded)");

        try {
            Base64Value value = Base64Value.createFromEncoded(encryptedBase64);
            return AsymmetricCipher.decrypt(AsymmetricCipher.ALGORITHM_RSA_ECB_OAEP256, null, key, value).toDecodedString();
        } catch (ConfigEncryptionException e) {
            throw e;
        } catch (Exception e) {
            throw new ConfigEncryptionException("Failed to decrypt value using RSA. Returning clear text value as is: "
                                                        + encryptedBase64, e);
        }
    }

    /**
     * Encrypt secret using RSA with OAEP.
     *
     * @param key     public key used to encrypt
     * @param secret  secret to encrypt
     * @return base64 encoded encrypted bytes
     * @throws ConfigEncryptionException If any problem with encryption occurs
     */
    public static String encryptRsa(PublicKey key, String secret) throws ConfigEncryptionException {
        Objects.requireNonNull(key, "Key must be provided for encryption");
        Objects.requireNonNull(secret, "Secret message must be provided to be encrypted");
        if (secret.getBytes(StandardCharsets.UTF_8).length > 190) {
            throw new ConfigEncryptionException("Secret value is too large. Maximum of 190 bytes is allowed.");
        }
        try {
            Base64Value value = Base64Value.create(secret);
            return AsymmetricCipher.encrypt(AsymmetricCipher.ALGORITHM_RSA_ECB_OAEP256, null, key, value).toBase64();
        } catch (Exception e) {
            throw new ConfigEncryptionException("Failed to encrypt using RSA key", e);
        }
    }

    /**
     * Encrypt using AES with GCM method, key is derived from password with random salt.
     *
     * @param masterPassword master password
     * @param secret         secret to encrypt
     * @return Encrypted value base64 encoded
     * @throws ConfigEncryptionException If any problem with encryption occurs
     */
    public static String encryptAes(char[] masterPassword, String secret) throws ConfigEncryptionException {
        Objects.requireNonNull(secret, "Secret message must be provided to be encrypted");

        return encryptAesBytes(masterPassword, secret.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * Encrypt using AES with GCM method, key is derived from password with random salt.
     *
     * @param masterPassword master password
     * @param secret         secret to encrypt
     * @return Encrypted value base64 encoded
     * @throws ConfigEncryptionException If any problem with encryption occurs
     * @deprecated this method will be removed once a separate module for encryption is created
     */
    @Deprecated(since = "2.2.0")
    public static String encryptAesBytes(char[] masterPassword, byte[] secret) throws ConfigEncryptionException {
        Objects.requireNonNull(masterPassword, "Password must be provided for encryption");
        Objects.requireNonNull(secret, "Secret message must be provided to be encrypted");

        byte[] salt = SECURE_RANDOM.get().generateSeed(SALT_LENGTH);
        byte[] nonce = SECURE_RANDOM.get().generateSeed(NONCE_LENGTH);
        byte[] key = PasswordKeyDerivation
                .deriveKey("PBKDF2WithHmacSHA256", null, masterPassword, salt, HASH_ITERATIONS, KEY_LENGTH);
        byte[] encrypted = SymmetricCipher.encrypt(SymmetricCipher.ALGORITHM_AES_GCM, key, nonce, Base64Value.create(secret))
                .toBytes();

        // get bytes to base64 (salt + nonce + encrypted message)
        byte[] bytesToEncode = new byte[encrypted.length + salt.length + nonce.length];
        System.arraycopy(salt, 0, bytesToEncode, 0, salt.length);
        System.arraycopy(nonce, 0, bytesToEncode, salt.length, nonce.length);
        System.arraycopy(encrypted, 0, bytesToEncode, nonce.length + salt.length, encrypted.length);

        return Base64.getEncoder().encodeToString(bytesToEncode);
    }

    /**
     * Decrypt using AES.
     * Will only decrypt messages encrypted with {@link #encryptAes(char[], String)} as the algorithm used is quite custom
     * (number of bytes of seed, of salt and approach).
     *
     * @param masterPassword  master password
     * @param encryptedBase64 encrypted secret, base64 encoded
     * @return Decrypted secret
     * @throws ConfigEncryptionException if something bad happens during decryption (e.g. wrong password)
     */
    public static String decryptAes(char[] masterPassword, String encryptedBase64) throws ConfigEncryptionException {
        return new String(decryptAesBytes(masterPassword, encryptedBase64), StandardCharsets.UTF_8);
    }

    /**
     * Decrypt using AES.
     * Will only decrypt messages encrypted with {@link #encryptAes(char[], String)} as the algorithm used is quite custom
     * (number of bytes of seed, of salt and approach).
     *
     * @param masterPassword  master password
     * @param encryptedBase64 encrypted secret, base64 encoded
     * @return Decrypted secret
     * @throws ConfigEncryptionException if something bad happens during decryption (e.g. wrong password)
     * @deprecated This method will be moved to a new module
     */
    @Deprecated(since = "2.2.0")
    public static byte[] decryptAesBytes(char[] masterPassword, String encryptedBase64) {
        Objects.requireNonNull(masterPassword, "Password must be provided for encryption");
        Objects.requireNonNull(encryptedBase64, "Encrypted bytes must be provided for decryption (base64 encoded)");

        try {
            // decode base64
            byte[] decodedBytes = Base64.getDecoder().decode(encryptedBase64);

            // extract salt and encrypted bytes
            byte[] salt = new byte[SALT_LENGTH];
            byte[] nonce = new byte[NONCE_LENGTH];
            byte[] encryptedBytes = new byte[decodedBytes.length - SALT_LENGTH - NONCE_LENGTH];

            System.arraycopy(decodedBytes, 0, salt, 0, SALT_LENGTH);
            System.arraycopy(decodedBytes, SALT_LENGTH, nonce, 0, NONCE_LENGTH);
            System.arraycopy(decodedBytes, SALT_LENGTH + NONCE_LENGTH, encryptedBytes, 0, encryptedBytes.length);

            byte[] key = PasswordKeyDerivation
                    .deriveKey("PBKDF2WithHmacSHA256", null, masterPassword, salt, HASH_ITERATIONS, KEY_LENGTH);
            Base64Value encryptedValue = Base64Value.create(encryptedBytes);
            return SymmetricCipher.decrypt(SymmetricCipher.ALGORITHM_AES_GCM, key, nonce, encryptedValue).toBytes();
        } catch (Throwable e) {
            throw new ConfigEncryptionException("Failed to decrypt value using AES. Returning clear text value as is: "
                                                        + encryptedBase64, e);
        }
    }

    static Optional resolveMasterPassword(boolean requireEncryption, org.eclipse.microprofile.config.Config config) {
        Optional result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE)
                .or(() -> {
                    Optional value = config.getOptionalValue(ConfigProperties.MASTER_PASSWORD_CONFIG_KEY, String.class);
                    if (value.isPresent()) {
                        if (requireEncryption) {
                            LOGGER.log(Level.WARNING,
                                       "Master password is configured as clear text in configuration when encryption is "
                                               + "required. "
                                               + "This value will be ignored. System property or environment variable "
                                               + "expected!!!");
                            return Optional.empty();
                        }
                    }
                    return value;
                })
                .map(String::toCharArray);

        if (result.isEmpty()) {
            LOGGER.log(Level.DEBUG, "Securing properties using master password is not available, as master password is not "
                    + "configured");
        }

        return result;
    }

    static Optional resolveMasterPassword(boolean requireEncryption, Config config) {
        Optional result = getEnv(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE)
                .or(() -> {
                    ConfigValue value = config.get(ConfigProperties.MASTER_PASSWORD_CONFIG_KEY).asString();
                    if (value.isPresent()) {
                        if (requireEncryption) {
                            LOGGER.log(Level.WARNING,
                                       "Master password is configured as clear text in configuration when encryption is "
                                               + "required. "
                                               + "This value will be ignored. System property or environment variable "
                                               + "expected!!!");
                            return Optional.empty();
                        }
                    }
                    return value.asOptional();
                })
                .map(String::toCharArray);

        if (!result.isPresent()) {
            LOGGER.log(Level.DEBUG, "Securing properties using master password is not available, as master password is not "
                    + "configured");
        }

        return result;
    }

    static Optional resolvePrivateKey(org.eclipse.microprofile.config.Config config) {
        return resolvePrivateKey(MpConfig.toHelidonConfig(config).get("security.config.rsa"));
    }

    static Optional resolvePrivateKey(Config config) {
        // load configuration values
        Keys.Builder builder = Keys.builder();
        builder.config(config);

        builder.pem(pemBuilder -> {
            getEnv(ConfigProperties.PRIVATE_KEY_PEM_PATH_ENV_VARIABLE)
                    .map(Paths::get)
                    .ifPresent(path -> pemBuilder.key(Resource.create(path)));

            getEnv(ConfigProperties.PRIVATE_KEY_PASS_ENV_VARIABLE)
                    .map(String::toCharArray)
                    .ifPresent(pemBuilder::keyPassphrase);
        });

        // override the ones defined in environment variables
        getEnv(ConfigProperties.PRIVATE_KEYSTORE_PATH_ENV_VARIABLE)
                .map(Paths::get)
                .ifPresent(path -> builder.keystore(keystoreBuilder -> {
                    keystoreBuilder.keystore(Resource.create(path));
                    getEnv(ConfigProperties.PRIVATE_KEYSTORE_TYPE_ENV_VARIABLE)
                            .ifPresent(keystoreBuilder::type);

                    getEnv(ConfigProperties.PRIVATE_KEYSTORE_PASS_ENV_VARIABLE)
                            .map(String::toCharArray)
                            .ifPresent(keystoreBuilder::passphrase);

                    getEnv(ConfigProperties.PRIVATE_KEY_PASS_ENV_VARIABLE)
                            .map(String::toCharArray)
                            .ifPresent(keystoreBuilder::keyPassphrase);

                    getEnv(ConfigProperties.PRIVATE_KEY_ALIAS_ENV_VARIABLE)
                            .ifPresent(keystoreBuilder::keyAlias);
                }));

        Optional result = builder.build().privateKey();

        if (result.isEmpty()) {
            LOGGER.log(Level.DEBUG, "Securing properties using asymmetric cipher is not available, as private key is not "
                    + "configured");
        }

        return result;
    }

    static Optional getEnv(String envVariable) {
        return Optional.ofNullable(System.getenv(envVariable));
    }

    private static Cipher cipherLegacy(char[] masterPassword, byte[] salt, int cipherMode) throws ConfigEncryptionException {
        try {
            SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
            KeySpec keySpec = new PBEKeySpec(masterPassword, salt, HASH_ITERATIONS, KEY_LENGTH_LEGACY);
            SecretKeySpec spec = new SecretKeySpec(secretKeyFactory.generateSecret(keySpec).getEncoded(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(cipherMode, spec, new IvParameterSpec(salt));

            return cipher;
        } catch (Exception e) {
            throw new ConfigEncryptionException("Failed to prepare a cipher instance", e);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy