001/* 002 * (C) Copyright 2015-2017 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 <[email protected]> 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 && 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}