001/*
002 * (C) Copyright 2015 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     jcarsique
016 */
017package org.nuxeo.common.codec;
018
019import java.io.BufferedReader;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.InputStreamReader;
023import java.io.Reader;
024import java.net.MalformedURLException;
025import java.net.URL;
026import java.security.GeneralSecurityException;
027import java.security.SecureRandom;
028import java.util.Arrays;
029import java.util.Enumeration;
030import java.util.Hashtable;
031import java.util.InvalidPropertiesFormatException;
032import java.util.List;
033import java.util.Map;
034import java.util.Objects;
035import java.util.Properties;
036import java.util.Random;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.function.BiFunction;
039
040import org.apache.commons.codec.binary.Base64;
041import org.apache.commons.lang.StringUtils;
042import org.apache.commons.lang3.ArrayUtils;
043import org.apache.commons.logging.Log;
044import org.apache.commons.logging.LogFactory;
045
046import org.nuxeo.common.Environment;
047
048/**
049 * {@link Properties} with crypto capabilities.<br>
050 * The cryptographic algorithms depend on:
051 * <ul>
052 * <li>Environment.SERVER_STATUS_KEY</li>
053 * <li>Environment.CRYPT_KEYALIAS && Environment.CRYPT_KEYSTORE_PATH || getProperty(Environment.JAVA_DEFAULT_KEYSTORE)</li>
054 * <li>Environment.CRYPT_KEY</li>
055 * </ul>
056 * Changing one of those parameters will affect the ability to read encrypted values.
057 *
058 * @see Crypto
059 * @since 7.4
060 */
061public class CryptoProperties extends Properties {
062    private static final Log log = LogFactory.getLog(CryptoProperties.class);
063
064    private Crypto crypto = Crypto.NO_OP;
065
066    private static final List<String> CRYPTO_PROPS = Arrays.asList(new String[] { Environment.SERVER_STATUS_KEY,
067            Environment.CRYPT_KEYALIAS, Environment.CRYPT_KEYSTORE_PATH, Environment.JAVA_DEFAULT_KEYSTORE,
068            Environment.CRYPT_KEYSTORE_PASS, Environment.JAVA_DEFAULT_KEYSTORE_PASS, Environment.CRYPT_KEY });
069
070    private byte[] cryptoID;
071
072    private static final int SALT_LEN = 8;
073
074    private final byte[] salt = new byte[SALT_LEN];
075
076    private static final Random random = new SecureRandom();
077
078    private Map<String, String> encrypted = new ConcurrentHashMap<>();
079
080    /**
081     * @param defaults
082     * @inherited {@link Properties#Properties(Properties)}
083     */
084    public CryptoProperties(Properties defaults) {
085        super(defaults);
086        synchronized (random) {
087            random.nextBytes(salt);
088        }
089        cryptoID = evalCryptoID();
090    }
091
092    private byte[] evalCryptoID() {
093        byte[] ID = null;
094        for (String prop : CRYPTO_PROPS) {
095            ID = ArrayUtils.addAll(ID, salt);
096            ID = ArrayUtils.addAll(ID, getProperty(prop, "").getBytes());
097        }
098        return crypto.getSHA1DigestOrEmpty(ID);
099    }
100
101    public CryptoProperties() {
102        this(null);
103    }
104
105    private static final long serialVersionUID = 1L;
106
107    public Crypto getCrypto() {
108        String statusKey = getProperty(Environment.SERVER_STATUS_KEY);
109        String keyAlias = getProperty(Environment.CRYPT_KEYALIAS);
110        String keystorePath = getProperty(Environment.CRYPT_KEYSTORE_PATH,
111                getProperty(Environment.JAVA_DEFAULT_KEYSTORE));
112        if (keyAlias != null && keystorePath != null) {
113            String keystorePass = getProperty(Environment.CRYPT_KEYSTORE_PASS);
114            if (!StringUtils.isEmpty(keystorePass)) {
115                keystorePass = new String(Base64.decodeBase64(keystorePass));
116            } else {
117                keystorePass = getProperty(Environment.JAVA_DEFAULT_KEYSTORE_PASS, "changeit");
118            }
119            try {
120                return new Crypto(keystorePath, keystorePass.toCharArray(), keyAlias, statusKey.toCharArray());
121            } catch (GeneralSecurityException | IOException e) {
122                log.warn(e);
123                return Crypto.NO_OP;
124            }
125        }
126
127        String secretKey = new String(Base64.decodeBase64(getProperty(Environment.CRYPT_KEY, "")));
128        if (!StringUtils.isEmpty(secretKey)) {
129            try (BufferedReader in = new BufferedReader(new InputStreamReader(new URL(secretKey).openStream()))) {
130                secretKey = in.readLine();
131            } catch (MalformedURLException e) {
132                // It's a raw value, not an URL => fall through
133            } catch (IOException e) {
134                log.warn(e);
135                return Crypto.NO_OP;
136            }
137        } else {
138            secretKey = statusKey;
139        }
140        if (secretKey == null) {
141            log.warn("Missing " + Environment.SERVER_STATUS_KEY);
142            return Crypto.NO_OP;
143        }
144        return new Crypto(secretKey.getBytes());
145    }
146
147    private boolean isNewCryptoProperty(String key, String value) {
148        return CRYPTO_PROPS.contains(key) && !StringUtils.equals(value, getProperty(key));
149    }
150
151    private void resetCrypto() {
152        byte[] id = evalCryptoID();
153        if (!Arrays.equals(id, cryptoID)) {
154            cryptoID = id;
155            crypto = getCrypto();
156        }
157    }
158
159    @Override
160    public synchronized void load(Reader reader) throws IOException {
161        Properties props = new Properties();
162        props.load(reader);
163        putAll(props);
164    }
165
166    @Override
167    public synchronized void load(InputStream inStream) throws IOException {
168        Properties props = new Properties();
169        props.load(inStream);
170        putAll(props);
171    }
172
173    protected class PropertiesGetDefaults extends Properties {
174        private static final long serialVersionUID = 1L;
175
176        public Properties getDefaults() {
177            return defaults;
178        }
179
180        public Hashtable<String, Object> getDefaultProperties() {
181            Hashtable<String, Object> h = new Hashtable<>();
182            if (defaults != null) {
183                Enumeration<?> allDefaultProperties = defaults.propertyNames();
184                while (allDefaultProperties.hasMoreElements()) {
185                    String key = (String) allDefaultProperties.nextElement();
186                    String value = defaults.getProperty(key);
187                    h.put(key, value);
188                }
189            }
190            return h;
191        }
192    }
193
194    @Override
195    public synchronized void loadFromXML(InputStream in) throws IOException, InvalidPropertiesFormatException {
196        PropertiesGetDefaults props = new PropertiesGetDefaults();
197        props.loadFromXML(in);
198        if (defaults == null) {
199            defaults = props.getDefaults();
200        } else {
201            defaults.putAll(props.getDefaultProperties());
202        }
203        putAll(props);
204    }
205
206    @Override
207    public synchronized Object put(Object key, Object value) {
208        Objects.requireNonNull(value);
209        String sKey = (String) key;
210        String sValue = (String) value;
211        if (isNewCryptoProperty(sKey, sValue)) { // Crypto properties are not themselves encrypted
212            Object old = super.put(sKey, sValue);
213            resetCrypto();
214            return old;
215        }
216        if (Crypto.isEncrypted(sValue)) {
217            encrypted.put(sKey, sValue);
218            sValue = new String(crypto.decrypt(sValue));
219        }
220        return super.put(sKey, sValue);
221    }
222
223    @Override
224    public synchronized void putAll(Map<? extends Object, ? extends Object> t) {
225        for (String key : CRYPTO_PROPS) {
226            if (t.containsKey(key)) {
227                super.put(key, t.get(key));
228            }
229        }
230        resetCrypto();
231        for (Map.Entry<? extends Object, ? extends Object> e : t.entrySet()) {
232            String key = (String) e.getKey();
233            String value = (String) e.getValue();
234            if (Crypto.isEncrypted(value)) {
235                encrypted.put(key, value);
236                value = new String(crypto.decrypt(value));
237            }
238            super.put(key, value);
239        }
240    }
241
242    @Override
243    public synchronized Object putIfAbsent(Object key, Object value) {
244        Objects.requireNonNull(value);
245        String sKey = (String) key;
246        String sValue = (String) value;
247        if (get(key) != null) { // Not absent: do nothing, return current value
248            return get(key);
249        }
250        if (isNewCryptoProperty(sKey, sValue)) { // Crypto properties are not themselves encrypted
251            Object old = super.putIfAbsent(sKey, sValue);
252            resetCrypto();
253            return old;
254        }
255        if (Crypto.isEncrypted(sValue)) {
256            encrypted.putIfAbsent(sKey, sValue);
257            sValue = new String(crypto.decrypt(sValue));
258        }
259        return super.putIfAbsent(sKey, sValue);
260    }
261
262    @Override
263    public synchronized boolean replace(Object key, Object oldValue, Object newValue) {
264        Objects.requireNonNull(oldValue);
265        Objects.requireNonNull(newValue);
266        String sKey = (String) key;
267        String sOldValue = (String) oldValue;
268        String sNewValue = (String) newValue;
269
270        if (isNewCryptoProperty(sKey, sNewValue)) { // Crypto properties are not themselves encrypted
271            if (super.replace(key, sOldValue, sNewValue)) {
272                resetCrypto();
273                return true;
274            } else {
275                return false;
276            }
277        }
278        if (super.replace(sKey, new String(crypto.decrypt(sOldValue)), new String(crypto.decrypt(sNewValue)))) {
279            if (Crypto.isEncrypted(sNewValue)) {
280                encrypted.put(sKey, sNewValue);
281            } else {
282                encrypted.remove(sKey);
283            }
284            return true;
285        }
286        return false;
287    }
288
289    @Override
290    public synchronized Object replace(Object key, Object value) {
291        Objects.requireNonNull(value);
292        if (!super.containsKey(key)) {
293            return null;
294        }
295        return put(key, value);
296    }
297
298    @Override
299    public synchronized Object merge(Object key, Object value,
300            BiFunction<? super Object, ? super Object, ? extends Object> remappingFunction) {
301        Objects.requireNonNull(remappingFunction);
302        // If the specified key is not already associated with a value or is associated with null, associates it with
303        // the given non-null value.
304        if (get(key) == null) {
305            putIfAbsent(key, value);
306            return value;
307        }
308        if (CRYPTO_PROPS.contains(key)) { // Crypto properties are not themselves encrypted
309            Object newValue = super.merge(key, value, remappingFunction);
310            resetCrypto();
311            return newValue;
312        }
313        String sKey = (String) key;
314        String sValue = (String) value;
315        if (Crypto.isEncrypted(sValue)) {
316            encrypted.put(sKey, sValue);
317            sValue = new String(crypto.decrypt(sValue));
318        }
319        return super.merge(sKey, sValue, remappingFunction);
320    }
321
322    /**
323     * @param key
324     * @return the "raw" property: not decrypted if it was provided encrypted
325     */
326    public String getRawProperty(String key) {
327        return getProperty(key, true);
328    }
329
330    /**
331     * Searches for the property with the specified key in this property list. If the key is not found in this property
332     * list, the default property list, and its defaults, recursively, are then checked. The method returns the default
333     * value argument if the property is not found.
334     *
335     * @param key
336     * @param defaultValue
337     * @return the "raw" property (not decrypted if it was provided encrypted) or the {@code defaultValue} if not found
338     * @see #setProperty
339     */
340    public String getRawProperty(String key, String defaultValue) {
341        String val = getRawProperty(key);
342        return (val == null) ? defaultValue : val;
343    }
344
345    @Override
346    public String getProperty(String key) {
347        return getProperty(key, false);
348    }
349
350    /**
351     * @param key
352     * @param raw if the encrypted values must be returned encrypted ({@code raw==true}) or decrypted ({@code raw==false}
353     *            )
354     * @return the property value or null
355     */
356    public String getProperty(String key, boolean raw) {
357        Object oval = super.get(key);
358        String value = (oval instanceof String) ? (String) oval : null;
359        if (value == null) {
360            if (defaults == null) {
361                encrypted.remove(key); // cleanup
362            } else if (defaults instanceof CryptoProperties) {
363                value = ((CryptoProperties) defaults).getProperty(key, raw);
364            } else {
365                value = defaults.getProperty(key);
366                if (Crypto.isEncrypted(value)) {
367                    encrypted.put(key, value);
368                    if (!raw) {
369                        value = new String(crypto.decrypt(value));
370                    }
371                }
372            }
373        } else if (raw && encrypted.containsKey(key)) {
374            value = encrypted.get(key);
375        }
376        return value;
377    }
378
379    @Override
380    public synchronized Object remove(Object key) {
381        encrypted.remove(key);
382        return super.remove(key);
383    }
384
385    @Override
386    public synchronized boolean remove(Object key, Object value) {
387        if (super.remove(key, value)) {
388            encrypted.remove(key);
389            return true;
390        }
391        return false;
392    }
393
394}