org.nuxeo.common.codec.CryptoProperties.java Source code

Java tutorial

Introduction

Here is the source code for org.nuxeo.common.codec.CryptoProperties.java

Source

/*
 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * 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.
 *
 * Contributors:
 *     jcarsique
 */
package org.nuxeo.common.codec;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.InvalidPropertiesFormatException;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiFunction;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.nuxeo.common.Environment;

/**
 * {@link Properties} with crypto capabilities.<br>
 * The cryptographic algorithms depend on:
 * <ul>
 * <li>Environment.SERVER_STATUS_KEY</li>
 * <li>Environment.CRYPT_KEYALIAS && Environment.CRYPT_KEYSTORE_PATH || getProperty(Environment.JAVA_DEFAULT_KEYSTORE)</li>
 * <li>Environment.CRYPT_KEY</li>
 * </ul>
 * Changing one of those parameters will affect the ability to read encrypted values.
 *
 * @see Crypto
 * @since 7.4
 */
public class CryptoProperties extends Properties {
    private static final Log log = LogFactory.getLog(CryptoProperties.class);

    private Crypto crypto = Crypto.NO_OP;

    private static final List<String> CRYPTO_PROPS = Arrays.asList(new String[] { Environment.SERVER_STATUS_KEY,
            Environment.CRYPT_KEYALIAS, Environment.CRYPT_KEYSTORE_PATH, Environment.JAVA_DEFAULT_KEYSTORE,
            Environment.CRYPT_KEYSTORE_PASS, Environment.JAVA_DEFAULT_KEYSTORE_PASS, Environment.CRYPT_KEY });

    private byte[] cryptoID;

    private static final int SALT_LEN = 8;

    private final byte[] salt = new byte[SALT_LEN];

    private static final Random random = new SecureRandom();

    private Map<String, String> encrypted = new ConcurrentHashMap<>();

    /**
     * @param defaults
     * @inherited {@link Properties#Properties(Properties)}
     */
    public CryptoProperties(Properties defaults) {
        super(defaults);
        synchronized (random) {
            random.nextBytes(salt);
        }
        cryptoID = evalCryptoID();
    }

    private byte[] evalCryptoID() {
        byte[] ID = null;
        for (String prop : CRYPTO_PROPS) {
            ID = ArrayUtils.addAll(ID, salt);
            ID = ArrayUtils.addAll(ID, getProperty(prop, "").getBytes());
        }
        return crypto.getSHA1DigestOrEmpty(ID);
    }

    public CryptoProperties() {
        this(null);
    }

    private static final long serialVersionUID = 1L;

    public Crypto getCrypto() {
        String statusKey = getProperty(Environment.SERVER_STATUS_KEY);
        String keyAlias = getProperty(Environment.CRYPT_KEYALIAS);
        String keystorePath = getProperty(Environment.CRYPT_KEYSTORE_PATH,
                getProperty(Environment.JAVA_DEFAULT_KEYSTORE));
        if (keyAlias != null && keystorePath != null) {
            String keystorePass = getProperty(Environment.CRYPT_KEYSTORE_PASS);
            if (!StringUtils.isEmpty(keystorePass)) {
                keystorePass = new String(Base64.decodeBase64(keystorePass));
            } else {
                keystorePass = getProperty(Environment.JAVA_DEFAULT_KEYSTORE_PASS, "changeit");
            }
            try {
                return new Crypto(keystorePath, keystorePass.toCharArray(), keyAlias, statusKey.toCharArray());
            } catch (GeneralSecurityException | IOException e) {
                log.warn(e);
                return Crypto.NO_OP;
            }
        }

        String secretKey = new String(Base64.decodeBase64(getProperty(Environment.CRYPT_KEY, "")));
        if (!StringUtils.isEmpty(secretKey)) {
            try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(secretKey).openStream()))) {
                secretKey = in.readLine();
            } catch (MalformedURLException e) {
                // It's a raw value, not an URL => fall through
            } catch (IOException e) {
                log.warn(e);
                return Crypto.NO_OP;
            }
        } else {
            secretKey = statusKey;
        }
        if (secretKey == null) {
            log.warn("Missing " + Environment.SERVER_STATUS_KEY);
            return Crypto.NO_OP;
        }
        return new Crypto(secretKey.getBytes());
    }

    private boolean isNewCryptoProperty(String key, String value) {
        return CRYPTO_PROPS.contains(key) && !StringUtils.equals(value, getProperty(key));
    }

    private void resetCrypto() {
        byte[] id = evalCryptoID();
        if (!Arrays.equals(id, cryptoID)) {
            cryptoID = id;
            crypto = getCrypto();
        }
    }

    @Override
    public synchronized void load(Reader reader) throws IOException {
        Properties props = new Properties();
        props.load(reader);
        putAll(props);
    }

    @Override
    public synchronized void load(InputStream inStream) throws IOException {
        Properties props = new Properties();
        props.load(inStream);
        putAll(props);
    }

    protected class PropertiesGetDefaults extends Properties {
        private static final long serialVersionUID = 1L;

        public Properties getDefaults() {
            return defaults;
        }

        public Hashtable<String, Object> getDefaultProperties() {
            Hashtable<String, Object> h = new Hashtable<>();
            if (defaults != null) {
                Enumeration<?> allDefaultProperties = defaults.propertyNames();
                while (allDefaultProperties.hasMoreElements()) {
                    String key = (String) allDefaultProperties.nextElement();
                    String value = defaults.getProperty(key);
                    h.put(key, value);
                }
            }
            return h;
        }
    }

    @Override
    public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException {
        PropertiesGetDefaults props = new PropertiesGetDefaults();
        props.loadFromXML(in);
        if (defaults == null) {
            defaults = props.getDefaults();
        } else {
            defaults.putAll(props.getDefaultProperties());
        }
        putAll(props);
    }

    @Override
    public synchronized Object put(Object key, Object value) {
        Objects.requireNonNull(value);
        String sKey = (String) key;
        String sValue = (String) value;
        if (isNewCryptoProperty(sKey, sValue)) { // Crypto properties are not themselves encrypted
            Object old = super.put(sKey, sValue);
            resetCrypto();
            return old;
        }
        if (Crypto.isEncrypted(sValue)) {
            encrypted.put(sKey, sValue);
            sValue = new String(crypto.decrypt(sValue));
        }
        return super.put(sKey, sValue);
    }

    @Override
    public synchronized void putAll(Map<? extends Object, ? extends Object> t) {
        for (String key : CRYPTO_PROPS) {
            if (t.containsKey(key)) {
                super.put(key, t.get(key));
            }
        }
        resetCrypto();
        for (Map.Entry<? extends Object, ? extends Object> e : t.entrySet()) {
            String key = (String) e.getKey();
            String value = (String) e.getValue();
            if (Crypto.isEncrypted(value)) {
                encrypted.put(key, value);
                value = new String(crypto.decrypt(value));
            }
            super.put(key, value);
        }
    }

    @Override
    public synchronized Object putIfAbsent(Object key, Object value) {
        Objects.requireNonNull(value);
        String sKey = (String) key;
        String sValue = (String) value;
        if (get(key) != null) { // Not absent: do nothing, return current value
            return get(key);
        }
        if (isNewCryptoProperty(sKey, sValue)) { // Crypto properties are not themselves encrypted
            Object old = super.putIfAbsent(sKey, sValue);
            resetCrypto();
            return old;
        }
        if (Crypto.isEncrypted(sValue)) {
            encrypted.putIfAbsent(sKey, sValue);
            sValue = new String(crypto.decrypt(sValue));
        }
        return super.putIfAbsent(sKey, sValue);
    }

    @Override
    public synchronized boolean replace(Object key, Object oldValue, Object newValue) {
        Objects.requireNonNull(oldValue);
        Objects.requireNonNull(newValue);
        String sKey = (String) key;
        String sOldValue = (String) oldValue;
        String sNewValue = (String) newValue;

        if (isNewCryptoProperty(sKey, sNewValue)) { // Crypto properties are not themselves encrypted
            if (super.replace(key, sOldValue, sNewValue)) {
                resetCrypto();
                return true;
            } else {
                return false;
            }
        }
        if (super.replace(sKey, new String(crypto.decrypt(sOldValue)), new String(crypto.decrypt(sNewValue)))) {
            if (Crypto.isEncrypted(sNewValue)) {
                encrypted.put(sKey, sNewValue);
            } else {
                encrypted.remove(sKey);
            }
            return true;
        }
        return false;
    }

    @Override
    public synchronized Object replace(Object key, Object value) {
        Objects.requireNonNull(value);
        if (!super.containsKey(key)) {
            return null;
        }
        return put(key, value);
    }

    @Override
    public synchronized Object merge(Object key, Object value,
            BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        // If the specified key is not already associated with a value or is associated with null, associates it with
        // the given non-null value.
        if (get(key) == null) {
            putIfAbsent(key, value);
            return value;
        }
        if (CRYPTO_PROPS.contains(key)) { // Crypto properties are not themselves encrypted
            Object newValue = super.merge(key, value, remappingFunction);
            resetCrypto();
            return newValue;
        }
        String sKey = (String) key;
        String sValue = (String) value;
        if (Crypto.isEncrypted(sValue)) {
            encrypted.put(sKey, sValue);
            sValue = new String(crypto.decrypt(sValue));
        }
        return super.merge(sKey, sValue, remappingFunction);
    }

    /**
     * @param key
     * @return the "raw" property: not decrypted if it was provided encrypted
     */
    public String getRawProperty(String key) {
        return getProperty(key, true);
    }

    /**
     * Searches for the property with the specified key in this property list. If the key is not found in this property
     * list, the default property list, and its defaults, recursively, are then checked. The method returns the default
     * value argument if the property is not found.
     *
     * @param key
     * @param defaultValue
     * @return the "raw" property (not decrypted if it was provided encrypted) or the {@code defaultValue} if not found
     * @see #setProperty
     */
    public String getRawProperty(String key, String defaultValue) {
        String val = getRawProperty(key);
        return (val == null) ? defaultValue : val;
    }

    @Override
    public String getProperty(String key) {
        return getProperty(key, false);
    }

    /**
     * @param key
     * @param raw if the encrypted values must be returned encrypted ({@code raw==true}) or decrypted ({@code raw==false}
     *            )
     * @return the property value or null
     */
    public String getProperty(String key, boolean raw) {
        Object oval = super.get(key);
        String value = (oval instanceof String) ? (String) oval : null;
        if (value == null) {
            if (defaults == null) {
                encrypted.remove(key); // cleanup
            } else if (defaults instanceof CryptoProperties) {
                value = ((CryptoProperties) defaults).getProperty(key, raw);
            } else {
                value = defaults.getProperty(key);
                if (Crypto.isEncrypted(value)) {
                    encrypted.put(key, value);
                    if (!raw) {
                        value = new String(crypto.decrypt(value));
                    }
                }
            }
        } else if (raw && encrypted.containsKey(key)) {
            value = encrypted.get(key);
        }
        return value;
    }

    @Override
    public synchronized Object remove(Object key) {
        encrypted.remove(key);
        return super.remove(key);
    }

    @Override
    public synchronized boolean remove(Object key, Object value) {
        if (super.remove(key, value)) {
            encrypted.remove(key);
            return true;
        }
        return false;
    }

}