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