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