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 <kleturc@nuxeo.com> 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 Crypto crypto = Crypto.NO_OP; 068 069 private static final List<String> CRYPTO_PROPS = Arrays.asList(Environment.SERVER_STATUS_KEY, 070 Environment.CRYPT_KEYALIAS, Environment.CRYPT_KEYSTORE_PATH, Environment.JAVA_DEFAULT_KEYSTORE, 071 Environment.CRYPT_KEYSTORE_PASS, Environment.JAVA_DEFAULT_KEYSTORE_PASS, Environment.CRYPT_KEY); 072 073 private byte[] cryptoID; 074 075 private static final int SALT_LEN = 8; 076 077 private final byte[] salt = new byte[SALT_LEN]; 078 079 private static final Random random = new SecureRandom(); 080 081 private Map<String, String> encrypted = new ConcurrentHashMap<>(); 082 083 /** 084 * {@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.isNotEmpty(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.isNotEmpty(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 * @return the "raw" property: not decrypted if it was provided encrypted 326 */ 327 public String getRawProperty(String key) { 328 return getProperty(key, true); 329 } 330 331 /** 332 * Searches for the property with the specified key in this property list. If the key is not found in this property 333 * list, the default property list, and its defaults, recursively, are then checked. The method returns the default 334 * value argument if the property is not found. 335 * 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 raw if the encrypted values must be returned encrypted ({@code raw==true}) or decrypted ( 351 * {@code raw==false} ) 352 * @return the property value or null 353 */ 354 public String getProperty(String key, boolean raw) { 355 Object oval = super.get(key); 356 String value = (oval instanceof String) ? (String) oval : null; 357 if (value == null) { 358 if (defaults == null) { 359 encrypted.remove(key); // cleanup 360 } else if (defaults instanceof CryptoProperties) { 361 value = ((CryptoProperties) defaults).getProperty(key, raw); 362 } else { 363 value = defaults.getProperty(key); 364 if (Crypto.isEncrypted(value)) { 365 encrypted.put(key, value); 366 if (!raw) { 367 value = new String(crypto.decrypt(value)); 368 } 369 } 370 } 371 } else if (raw && encrypted.containsKey(key)) { 372 value = encrypted.get(key); 373 } 374 return value; 375 } 376 377 @Override 378 public synchronized Object remove(Object key) { 379 encrypted.remove(key); 380 return super.remove(key); 381 } 382 383 @Override 384 public synchronized boolean remove(Object key, Object value) { 385 if (super.remove(key, value)) { 386 encrypted.remove(key); 387 return true; 388 } 389 return false; 390 } 391 392}