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}