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}