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}