/*
* (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;
}
}