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.File; 023import java.io.FileInputStream; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.nio.ByteBuffer; 029import java.nio.CharBuffer; 030import java.nio.charset.Charset; 031import java.security.GeneralSecurityException; 032import java.security.InvalidKeyException; 033import java.security.KeyStore; 034import java.security.KeyStore.PasswordProtection; 035import java.security.KeyStoreException; 036import java.security.MessageDigest; 037import java.security.NoSuchAlgorithmException; 038import java.security.Security; 039import java.util.Arrays; 040import java.util.HashMap; 041import java.util.Map; 042import java.util.regex.Matcher; 043import java.util.regex.Pattern; 044 045import javax.crypto.BadPaddingException; 046import javax.crypto.Cipher; 047import javax.crypto.IllegalBlockSizeException; 048import javax.crypto.NoSuchPaddingException; 049import javax.crypto.SecretKey; 050import javax.crypto.spec.SecretKeySpec; 051 052import org.apache.commons.codec.binary.Base64; 053import org.apache.commons.lang3.StringUtils; 054import org.apache.commons.logging.Log; 055import org.apache.commons.logging.LogFactory; 056 057/** 058 * Supported algorithms (name, keysize): 059 * <ul> 060 * <li>AES/ECB/PKCS5Padding (128)</li> 061 * <li>DES/ECB/PKCS5Padding (64)</li> 062 * <ul/> 063 * 064 * @since 7.4 065 */ 066public class Crypto { 067 068 protected static final Pattern CRYPTO_PATTERN = Pattern.compile("\\{\\$(?<algo>.*)\\$(?<value>.+)\\}"); 069 070 private static final Log log = LogFactory.getLog(Crypto.class); 071 072 public static final String AES = "AES"; 073 074 public static final String AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding"; 075 076 public static final String DES = "DES"; 077 078 public static final String DES_ECB_PKCS5PADDING = "DES/ECB/PKCS5Padding"; 079 080 public static final String[] IMPLEMENTED_ALGOS = { AES, DES, AES_ECB_PKCS5PADDING, DES_ECB_PKCS5PADDING }; 081 082 public static final String DEFAULT_ALGO = AES_ECB_PKCS5PADDING; 083 084 private static final String SHA1 = "SHA-1"; 085 086 private final byte[] secretKey; 087 088 private final Map<String, SecretKey> secretKeys = new HashMap<>(); 089 090 private boolean initialized = true; 091 092 private final byte[] digest; 093 094 public Crypto(byte[] secretKey) { 095 this.secretKey = secretKey; 096 digest = getSHA1DigestOrEmpty(secretKey); 097 if (digest.length == 0) { 098 clear(); 099 } 100 } 101 102 /** 103 * Initialize cryptography with a map of {@link SecretKey}. 104 * 105 * @param secretKeys Map of {@code SecretKey} per algorithm 106 */ 107 public Crypto(Map<String, SecretKey> secretKeys) { 108 this(secretKeys, Crypto.class.getName().toCharArray()); 109 } 110 111 /** 112 * Initialize cryptography with a map of {@link SecretKey}. 113 * 114 * @param digest Digest for later use by {@link #verifyKey(byte[])} 115 * @param secretKeys Map of {@code SecretKey} per algorithm 116 */ 117 public Crypto(Map<String, SecretKey> secretKeys, char[] digest) { 118 secretKey = new byte[0]; 119 this.digest = getSHA1DigestOrEmpty(getBytes(digest)); 120 this.secretKeys.putAll(secretKeys); 121 if (this.digest.length == 0) { 122 clear(); 123 } 124 } 125 126 /** 127 * Initialize cryptography with a keystore. 128 * 129 * @param keystorePath Path to the keystore. 130 * @param keystorePass Keystore password. It is also used to generate the digest for {@link #verifyKey(byte[])} 131 * @param keyAlias Key alias prefix. It is suffixed with the algorithm. 132 * @param keyPass Key password 133 * @throws IOException 134 * @throws GeneralSecurityException 135 */ 136 public Crypto(String keystorePath, char[] keystorePass, String keyAlias, char[] keyPass) 137 throws GeneralSecurityException, IOException { 138 this(Crypto.getKeysFromKeyStore(keystorePath, keystorePass, keyAlias, keyPass), keystorePass); 139 } 140 141 private final static class NO_OP extends Crypto { 142 private NO_OP() { 143 super(new byte[0]); 144 } 145 146 @Override 147 public String encrypt(String algorithm, byte[] bytesToEncrypt) throws GeneralSecurityException { 148 return null; 149 } 150 151 @Override 152 public byte[] decrypt(String strToDecrypt) { 153 return strToDecrypt.getBytes(); 154 } 155 156 @Override 157 public void clear() { 158 // NO OP 159 } 160 } 161 162 public static final Crypto NO_OP = new NO_OP(); 163 164 protected SecretKey getSecretKey(String algorithm, byte[] key) throws NoSuchAlgorithmException { 165 if (!initialized) { 166 throw new RuntimeException("The Crypto object has been cleared."); 167 } 168 if (AES_ECB_PKCS5PADDING.equals(algorithm)) { 169 algorithm = AES; // AES_ECB_PKCS5PADDING is the default for AES 170 } else if (DES_ECB_PKCS5PADDING.equals(algorithm)) { 171 algorithm = DES; // DES_ECB_PKCS5PADDING is the default for DES 172 } 173 if (!secretKeys.containsKey(algorithm)) { 174 if (secretKey.length == 0) { 175 throw new NoSuchAlgorithmException("Unsupported algorithm: " + algorithm); 176 } 177 if (AES.equals(algorithm)) { // default for AES 178 key = Arrays.copyOf(getSHA1Digest(key), 16); // use a 128 bits secret key 179 secretKeys.put(AES, new SecretKeySpec(key, AES)); 180 } else if (DES.equals(algorithm)) { // default for DES 181 key = Arrays.copyOf(getSHA1Digest(key), 8); // use a 64 bits secret key 182 secretKeys.put(DES, new SecretKeySpec(key, DES)); 183 } else { 184 throw new NoSuchAlgorithmException("Unsupported algorithm: " + algorithm); 185 } 186 } 187 return secretKeys.get(algorithm); 188 } 189 190 public byte[] getSHA1Digest(final byte[] key) throws NoSuchAlgorithmException { 191 MessageDigest sha = MessageDigest.getInstance(SHA1); 192 return sha.digest(key); 193 } 194 195 public byte[] getSHA1DigestOrEmpty(final byte[] bytes) { 196 byte[] aDigest = new byte[0]; 197 try { 198 aDigest = getSHA1Digest(bytes); 199 } catch (NoSuchAlgorithmException e) { 200 log.error(e); 201 } 202 return aDigest; 203 } 204 205 public String encrypt(byte[] bytesToEncrypt) throws GeneralSecurityException { 206 return encrypt(null, bytesToEncrypt); 207 } 208 209 /** 210 * @param algorithm cipher transformation of the form "algorithm/mode/padding" or "algorithm". See the Cipher 211 * section in the <a 212 * href=http://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#Cipher>Java 213 * Cryptography Architecture Standard Algorithm Name Documentation</a>. 214 * @throws NoSuchPaddingException if {@code algorithm} contains a padding scheme that is not available. 215 * @throws NoSuchAlgorithmException if {@code algorithm} is in an invalid or not supported format. 216 * @throws GeneralSecurityException 217 */ 218 public String encrypt(String algorithm, byte[] bytesToEncrypt) throws GeneralSecurityException { 219 final String encryptedAlgo; 220 if (StringUtils.isBlank(algorithm)) { 221 algorithm = DEFAULT_ALGO; 222 encryptedAlgo = ""; 223 } else { 224 encryptedAlgo = Base64.encodeBase64String(algorithm.getBytes()); 225 } 226 Cipher cipher = Cipher.getInstance(algorithm); 227 cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(algorithm, secretKey)); 228 final String encryptedString = Base64.encodeBase64String(cipher.doFinal(bytesToEncrypt)); 229 return String.format("{$%s$%s}", encryptedAlgo, encryptedString); 230 } 231 232 /** 233 * The method returns either the decrypted {@code strToDecrypt}, either the {@code strToDecrypt} itself if it is not 234 * recognized as a crypted string or if the decryption fails. The return value is a byte array for security purpose, 235 * it is your responsibility to convert it then to a String or not (use of {@code char[]} is recommended). 236 * 237 * @return the decrypted {@code strToDecrypt} as an array of bytes, never {@code null} 238 * @see #getChars(byte[]) 239 */ 240 public byte[] decrypt(String strToDecrypt) { 241 Matcher matcher = CRYPTO_PATTERN.matcher(strToDecrypt); 242 if (!matcher.matches()) { 243 return strToDecrypt.getBytes(); 244 } 245 Cipher decipher; 246 try { 247 String algorithm = new String(Base64.decodeBase64(matcher.group("algo"))); 248 if (StringUtils.isBlank(algorithm)) { 249 algorithm = DEFAULT_ALGO; 250 } 251 decipher = Cipher.getInstance(algorithm); 252 decipher.init(Cipher.DECRYPT_MODE, getSecretKey(algorithm, secretKey)); 253 return decipher.doFinal(Base64.decodeBase64(matcher.group("value"))); 254 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 255 log.trace("Available algorithms: " + Security.getAlgorithms("Cipher")); 256 log.trace("Available security providers: " + Arrays.asList(Security.getProviders())); 257 log.debug(e, e); 258 } catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { 259 log.debug(e, e); 260 } 261 return strToDecrypt.getBytes(); 262 } 263 264 /** 265 * Clear sensible values. That makes the current object unusable. 266 */ 267 public void clear() { 268 Arrays.fill(secretKey, (byte) 0); 269 Arrays.fill(digest, (byte) 0); 270 secretKeys.clear(); 271 initialized = false; 272 } 273 274 @Override 275 protected void finalize() throws Throwable { 276 clear(); 277 super.finalize(); 278 } 279 280 /** 281 * Test the given {@code candidateDigest} against the configured digest. In case of failure, the secret data is 282 * destroyed and the object is made unusable.<br> 283 * Use that method to check if some code is allowed to request that Crypto object. 284 * 285 * @return true if {@code candidateDigest} matches the one used on creation. 286 * @see #clear() 287 * @see #verifyKey(char[]) 288 */ 289 public boolean verifyKey(byte[] candidateDigest) { 290 boolean success = Arrays.equals(getSHA1DigestOrEmpty(candidateDigest), digest); 291 if (!success) { 292 clear(); 293 } 294 return success; 295 } 296 297 /** 298 * Test the given {@code candidateDigest} against the configured digest. In case of failure, the secret data is 299 * destroyed and the object is made unusable.<br> 300 * Use that method to check if some code is allowed to request that Crypto object. 301 * 302 * @return true if {@code candidateDigest} matches the one used on creation. 303 * @see #clear() 304 * @see #verifyKey(byte[]) 305 */ 306 public boolean verifyKey(char[] candidateDigest) { 307 return verifyKey(getBytes(candidateDigest)); 308 } 309 310 /** 311 * Utility method to get {@code byte[]} from {@code char[]} since it is recommended to store passwords in 312 * {@code char[]} rather than in {@code String}.<br> 313 * The default charset of this Java virtual machine is used. There can be conversion issue with unmappable 314 * characters: they will be replaced with the charset's default replacement string. 315 * 316 * @param chars char array to convert 317 * @return the byte array converted from {@code chars} using the default charset. 318 */ 319 public static byte[] getBytes(char[] chars) { 320 CharBuffer charBuffer = CharBuffer.wrap(chars); 321 ByteBuffer byteBuffer = Charset.defaultCharset().encode(charBuffer); 322 return Arrays.copyOfRange(byteBuffer.array(), 0, byteBuffer.limit()); 323 } 324 325 /** 326 * Utility method to get {@code char[]} from {@code bytes[]} since it is recommended to store passwords in 327 * {@code char[]} rather than in {@code String}.<br> 328 * The default charset of this Java virtual machine is used. There can be conversion issue with unmappable 329 * characters: they will be replaced with the charset's default replacement string. 330 * 331 * @param bytes byte array to convert 332 * @return the char array converted from {@code bytes} using the default charset. 333 */ 334 public static char[] getChars(byte[] bytes) { 335 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 336 CharBuffer charBuffer = Charset.defaultCharset().decode(byteBuffer); 337 return Arrays.copyOfRange(charBuffer.array(), 0, charBuffer.limit()); 338 } 339 340 /** 341 * @return true if the given {@code value} is encrypted 342 */ 343 public static boolean isEncrypted(String value) { 344 return value != null && CRYPTO_PATTERN.matcher(value).matches(); 345 } 346 347 /** 348 * Extract secret keys from a keystore looking for {@code keyAlias + algorithm} 349 * 350 * @param keystorePath Path to the keystore 351 * @param keystorePass Keystore password 352 * @param keyAlias Key alias prefix. It is suffixed with the algorithm. 353 * @param keyPass Key password 354 * @throws GeneralSecurityException 355 * @throws IOException 356 * @see #IMPLEMENTED_ALGOS 357 */ 358 public static Map<String, SecretKey> getKeysFromKeyStore(String keystorePath, char[] keystorePass, String keyAlias, 359 char[] keyPass) throws GeneralSecurityException, IOException { 360 KeyStore keystore = KeyStore.getInstance("JCEKS"); 361 try (InputStream keystoreStream = new FileInputStream(keystorePath)) { 362 keystore.load(keystoreStream, keystorePass); 363 } 364 Map<String, SecretKey> secretKeys = new HashMap<>(); 365 for (String algo : IMPLEMENTED_ALGOS) { 366 if (keystore.containsAlias(keyAlias + algo)) { 367 SecretKey key = (SecretKey) keystore.getKey(keyAlias + algo, keyPass); 368 secretKeys.put(algo, key); 369 } 370 } 371 if (secretKeys.isEmpty()) { 372 throw new KeyStoreException(String.format("No alias \"%s<algo>\" found in %s", keyAlias, keystorePath)); 373 } 374 return secretKeys; 375 } 376 377 /** 378 * Store a key in a keystore.<br> 379 * The keystore is created if it doesn't exist. 380 * 381 * @param keystorePath Path to the keystore 382 * @param keystorePass Keystore password 383 * @param keyAlias Key alias prefix. It must be suffixed with the algorithm ({@link SecretKey#getAlgorithm()} is 384 * fine). 385 * @param keyPass Key password 386 * @throws GeneralSecurityException 387 * @throws IOException 388 * @see #IMPLEMENTED_ALGOS 389 */ 390 public static void setKeyInKeyStore(String keystorePath, char[] keystorePass, String keyAlias, char[] keyPass, 391 SecretKey key) throws GeneralSecurityException, IOException { 392 KeyStore keystore = KeyStore.getInstance("JCEKS"); 393 if (!new File(keystorePath).exists()) { 394 log.info("Creating a new JCEKS keystore at " + keystorePath); 395 keystore.load(null); 396 } else { 397 try (InputStream keystoreStream = new FileInputStream(keystorePath)) { 398 keystore.load(keystoreStream, keystorePass); 399 } 400 } 401 KeyStore.SecretKeyEntry keyStoreEntry = new KeyStore.SecretKeyEntry(key); 402 PasswordProtection keyPassword = new PasswordProtection(keyPass); 403 keystore.setEntry(keyAlias, keyStoreEntry, keyPassword); 404 try (OutputStream keystoreStream = new FileOutputStream(keystorePath)) { 405 keystore.store(keystoreStream, keystorePass); 406 } 407 } 408 409}