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