PrometheusPreferenceSecurity.java

/*
 * Prometheus: Application Framework
 * Copyright 2012-2026. Tony Washer
 *
 * 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.github.tonywasher.joceanus.prometheus.preference;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianLength;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactoryType;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySetSpec;
import io.github.tonywasher.joceanus.gordianknot.api.lock.GordianKeySetLock;
import io.github.tonywasher.joceanus.gordianknot.api.lock.GordianLockFactory;
import io.github.tonywasher.joceanus.gordianknot.api.lock.GordianPasswordLockSpec;
import io.github.tonywasher.joceanus.gordianknot.util.GordianGenerator;
import io.github.tonywasher.joceanus.metis.preference.MetisPreferenceKey;
import io.github.tonywasher.joceanus.metis.preference.MetisPreferenceManager;
import io.github.tonywasher.joceanus.metis.preference.MetisPreferenceResource;
import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
import io.github.tonywasher.joceanus.prometheus.exc.PrometheusSecurityException;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.EnumSet;
import java.util.Set;

/**
 * Security for Preferences.
 */
public class PrometheusPreferenceSecurity {
    /**
     * Logger.
     */
    private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(PrometheusPreferenceSecurity.class);

    /**
     * Default KeyLength.
     */
    private static final GordianLength DEFAULT_KEYLEN = GordianLength.LEN_256;

    /**
     * The KeySet.
     */
    private final GordianKeySet theKeySet;

    /**
     * Constructor.
     *
     * @param pManager the preference manager
     * @throws OceanusException on error
     */
    PrometheusPreferenceSecurity(final PrometheusPreferenceManager pManager) throws OceanusException {
        /* Protect against exceptions */
        try {
            /* Create a Security Factory */
            final GordianFactory myFactory = GordianGenerator.createFactory(GordianFactoryType.BC);
            final GordianLockFactory myLocks = myFactory.getLockFactory();

            /* Obtain the hash as a preference */
            final PrometheusBaseSecurityPreferences myPrefs = pManager.getPreferenceSet(PrometheusBaseSecurityPreferences.class);
            final byte[] myLock = myPrefs.getByteArrayValue(PrometheusSecurityPreferenceKey.LOCK);

            /* Derive the password */
            final char[] myHost = getHostName();
            final char[] myUser = System.getProperty("user.name").toCharArray();
            final char[] myPassword = new char[myHost.length + myUser.length];
            System.arraycopy(myHost, 0, myPassword, 0, myHost.length);
            System.arraycopy(myUser, 0, myPassword, myHost.length, myUser.length);

            /* Derive or create the lock */
            final GordianKeySetLock myKeySetLock = myLock == null
                    ? myLocks.newKeySetLock(new GordianPasswordLockSpec(), myPassword)
                    : myLocks.resolveKeySetLock(myLock, myPassword);

            /* record the KeySet */
            theKeySet = myKeySetLock.getKeySet();

            /* If we have created a new lock */
            if (myLock == null) {
                /* Record the lock */
                myPrefs.setHash(myKeySetLock.getLockBytes());
                myPrefs.storeChanges();
            }
        } catch (GordianException e) {
            throw new PrometheusSecurityException(e);
        }
    }

    /**
     * Encrypt the value.
     *
     * @param pValue the value to encrypt
     * @return the encrypted value
     * @throws OceanusException on error
     */
    protected byte[] encryptValue(final char[] pValue) throws OceanusException {
        /* Protect against exceptions */
        try {
            final byte[] myBytes = OceanusDataConverter.charsToByteArray(pValue);
            return theKeySet.encryptBytes(myBytes);
        } catch (GordianException e) {
            throw new PrometheusSecurityException(e);
        }
    }

    /**
     * Decrypt the value.
     *
     * @param pValue the value to decrypt
     * @return the decrypted value
     * @throws OceanusException on error
     */
    protected char[] decryptValue(final byte[] pValue) throws OceanusException {
        /* Protect against exceptions */
        try {
            final byte[] myBytes = theKeySet.decryptBytes(pValue);
            return OceanusDataConverter.bytesToCharArray(myBytes);
        } catch (GordianException e) {
            throw new PrometheusSecurityException(e);
        }
    }

    /**
     * determine hostName.
     *
     * @return the hostName
     */
    private static char[] getHostName() {
        /* Protect against exceptions */
        try {
            final InetAddress myAddr = InetAddress.getLocalHost();
            return myAddr.getHostName().toCharArray();

        } catch (UnknownHostException e) {
            LOGGER.error("Hostname can not be resolved", e);
            return "localhost".toCharArray();
        }
    }

    /**
     * SecurityPreferenceKey.
     */
    public enum PrometheusSecurityPreferenceKey implements MetisPreferenceKey {
        /**
         * Lock.
         */
        LOCK("Lock", null),

        /**
         * Factory.
         */
        FACTORY("FactoryType", MetisPreferenceResource.SECPREF_FACTORY),

        /**
         * KeyLength.
         */
        KEYLENGTH("KeyLength", MetisPreferenceResource.SECPREF_KEYLEN),

        /**
         * Cipher Steps.
         */
        CIPHERSTEPS("CipherSteps", MetisPreferenceResource.SECPREF_CIPHERSTEPS),

        /**
         * Hash Iterations.
         */
        HASHITERATIONS("HashIterations", MetisPreferenceResource.SECPREF_ITERATIONS),

        /**
         * ActiveKeySets.
         */
        ACTIVEKEYSETS("NumActiveKeySets", MetisPreferenceResource.SECPREF_KEYSETS);

        /**
         * The name of the Preference.
         */
        private final String theName;

        /**
         * The display string.
         */
        private final String theDisplay;

        /**
         * Constructor.
         *
         * @param pName    the name
         * @param pDisplay the display resource
         */
        PrometheusSecurityPreferenceKey(final String pName,
                                        final MetisPreferenceResource pDisplay) {
            theName = pName;
            theDisplay = pDisplay != null
                    ? pDisplay.getValue()
                    : null;
        }

        @Override
        public String getName() {
            return theName;
        }

        @Override
        public String getDisplay() {
            return theDisplay;
        }
    }

    /**
     * PrefSecurityPreferences.
     */
    public static class PrometheusBaseSecurityPreferences
            extends PrometheusPreferenceSet {
        /**
         * Constructor.
         *
         * @param pManager the preference manager
         * @throws OceanusException on error
         */
        public PrometheusBaseSecurityPreferences(final MetisPreferenceManager pManager) throws OceanusException {
            super((PrometheusPreferenceManager) pManager, MetisPreferenceResource.SECPREF_BASEPREFNAME);
            setHidden();
        }

        /**
         * Set lock.
         *
         * @param pHash the lock
         */
        protected void setHash(final byte[] pHash) {
            getByteArrayPreference(PrometheusSecurityPreferenceKey.LOCK).setValue(pHash);
        }

        @Override
        protected void definePreferences() {
            defineByteArrayPreference(PrometheusSecurityPreferenceKey.LOCK);
        }

        @Override
        public void autoCorrectPreferences() {
            /* No-OP */
        }
    }

    /**
     * PrefSecurityPreferences.
     */
    public static class PrometheusSecurityPreferences
            extends PrometheusPreferenceSet {
        /**
         * Valid lengths.
         */
        private static final Set<GordianLength> VALID_LENGTHS = EnumSet.of(GordianLength.LEN_128, GordianLength.LEN_192, GordianLength.LEN_256);

        /**
         * Default Security Phrase.
         */
        private static final String DEFAULT_SECURITY_PHRASE = "PleaseChangeMeToSomethingMoreUnique";

        /**
         * Minimum Number of Active KeySets.
         */
        private static final int MINIMUM_ACTIVE_KEYSETS = 4;

        /**
         * Maximum Number of Active KeySets.
         */
        private static final int MAXIMUM_ACTIVE_KEYSETS = 64;

        /**
         * Default Number of Active KeySets.
         */
        private static final int DEFAULT_ACTIVE_KEYSETS = 8;

        /**
         * Constructor.
         *
         * @param pManager the preference manager
         * @throws OceanusException on error
         */
        public PrometheusSecurityPreferences(final MetisPreferenceManager pManager) throws OceanusException {
            super((PrometheusPreferenceManager) pManager, MetisPreferenceResource.SECPREF_PREFNAME);
        }

        /**
         * Get FactoryType.
         *
         * @return the factoryType
         */
        public GordianFactoryType getFactoryType() {
            return getEnumValue(PrometheusSecurityPreferenceKey.FACTORY, GordianFactoryType.class);
        }

        /**
         * Get KeySetSpec.
         *
         * @return the spec
         */
        public GordianKeySetSpec getKeySetSpec() {
            /* Build and return keySetSpec */
            final GordianLength myKeyLen = getEnumValue(PrometheusSecurityPreferenceKey.KEYLENGTH, GordianLength.class);
            final int mySteps = getIntegerValue(PrometheusSecurityPreferenceKey.CIPHERSTEPS);
            return new GordianKeySetSpec(myKeyLen, mySteps);
        }

        /**
         * Get PasswordLockSpec.
         *
         * @return the spec
         */
        public GordianPasswordLockSpec getPasswordLockSpec() {
            /* Build and return keySetSpec */
            final int myIterations = getIntegerValue(PrometheusSecurityPreferenceKey.HASHITERATIONS);
            return new GordianPasswordLockSpec(myIterations, getKeySetSpec());
        }

        @Override
        protected void definePreferences() throws OceanusException {
            defineEnumPreference(PrometheusSecurityPreferenceKey.FACTORY, GordianFactoryType.class);
            defineEnumPreference(PrometheusSecurityPreferenceKey.KEYLENGTH, GordianLength.class);
            defineIntegerPreference(PrometheusSecurityPreferenceKey.CIPHERSTEPS);
            defineIntegerPreference(PrometheusSecurityPreferenceKey.HASHITERATIONS);
            defineIntegerPreference(PrometheusSecurityPreferenceKey.ACTIVEKEYSETS);
        }

        @Override
        public void autoCorrectPreferences() {
            /* Make sure that the factory is specified */
            final MetisEnumPreference<GordianFactoryType> myFactPref
                    = getEnumPreference(PrometheusSecurityPreferenceKey.FACTORY, GordianFactoryType.class);
            if (!myFactPref.isAvailable()) {
                myFactPref.setValue(GordianFactoryType.BC);
            }

            /* Make sure that the restricted state is specified */
            final MetisEnumPreference<GordianLength> myLengthPref
                    = getEnumPreference(PrometheusSecurityPreferenceKey.KEYLENGTH, GordianLength.class);
            if (!myLengthPref.isAvailable()) {
                myLengthPref.setValue(DEFAULT_KEYLEN);
            }

            /* Make sure that the length is restricted */
            myLengthPref.setFilter(VALID_LENGTHS::contains);

            /* Make sure that the cipherSteps is specified */
            MetisIntegerPreference myPref = getIntegerPreference(PrometheusSecurityPreferenceKey.CIPHERSTEPS);
            if (!myPref.isAvailable()) {
                myPref.setValue(GordianKeySetSpec.DEFAULT_CIPHER_STEPS);
            }

            /* Define the range */
            myPref.setRange(GordianKeySetSpec.MINIMUM_CIPHER_STEPS, GordianKeySetSpec.MAXIMUM_CIPHER_STEPS);
            if (!myPref.validate()) {
                myPref.setValue(GordianKeySetSpec.DEFAULT_CIPHER_STEPS);
            }

            /* Make sure that the hashIterations is specified */
            myPref = getIntegerPreference(PrometheusSecurityPreferenceKey.HASHITERATIONS);
            if (!myPref.isAvailable()) {
                myPref.setValue(GordianPasswordLockSpec.DEFAULT_ITERATIONS);
            }

            /* Define the range */
            myPref.setRange(GordianPasswordLockSpec.MINIMUM_ITERATIONS, GordianPasswordLockSpec.MAXIMUM_ITERATIONS);
            if (!myPref.validate()) {
                myPref.setValue(GordianPasswordLockSpec.DEFAULT_ITERATIONS);
            }

            /* Make sure that the activeKeySets is specified */
            myPref = getIntegerPreference(PrometheusSecurityPreferenceKey.ACTIVEKEYSETS);
            if (!myPref.isAvailable()) {
                myPref.setValue(DEFAULT_ACTIVE_KEYSETS);
            }

            /* Define the range */
            myPref.setRange(MINIMUM_ACTIVE_KEYSETS, MAXIMUM_ACTIVE_KEYSETS);
            if (!myPref.validate()) {
                myPref.setValue(DEFAULT_ACTIVE_KEYSETS);
            }
        }
    }
}