CryptoHelperFactory.java

/*
 * okhttp-client-extensions - A set of helpful extensions to support okhttp clients
 * Copyright © 2025 Andy Miles (andy.miles@amilesend.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU 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 Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.amilesend.client.crypto;

import lombok.Builder;
import lombok.NonNull;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.security.NoSuchAlgorithmException;

/**
 * Factory used to vend a configured {@link CryptoHelper} instance.
 * @see CryptoHelper
 */
@Builder
public class CryptoHelperFactory {
    private static final String ALIAS = "onedrive-auth-crypto-key";
    private static final String KEY_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
    private static final String KEY_ALGORITHM = "AES";
    private static final int KEY_SIZE = 128;

    /** The key store helper used to store/retrieve keys. */
    @NonNull
    private final KeyStoreHelper keyStoreHelper;
    /** The password for the keystore. */
    @NonNull
    private final char[] keyPassword;
    /** The key alias that is stored in the keystore. */
    @Builder.Default
    @NonNull
    private final String keyAlias = ALIAS;

    /**
     * Creates a new {@link CryptoHelper} instance.
     *
     * @return the crypto helper instance
     * @throws CryptoHelperException if an error occurred while creating the instance
     */
    public CryptoHelper newInstance() throws CryptoHelperException {
        try {
            SecretKey cryptoKey = keyStoreHelper.getSecretKey(keyAlias, keyPassword);
            // Not using Optional#ofNullable() here so that exceptions are propagated
            if (cryptoKey == null) {
                cryptoKey = generateNewCryptoKey();
                keyStoreHelper.saveSecretKey(keyAlias, cryptoKey, keyPassword);
            }

            return new CryptoHelper(KEY_CIPHER_ALGORITHM, cryptoKey);
        } catch (final KeyStoreHelperException ex) {
            throw new CryptoHelperException("Unable to retrieve key from keystore: " + ex.getMessage(), ex);
        } catch (final NoSuchAlgorithmException ex) {
            throw new CryptoHelperException(
                    "Unable to generate a new crypto key (malformed algorithm): " + ex.getMessage(), ex);
        }
    }

    private SecretKey generateNewCryptoKey() throws NoSuchAlgorithmException {
        final KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
        keyGenerator.init(KEY_SIZE);
        return keyGenerator.generateKey();
    }
}