001/* 002 * (C) Copyright 2006-2014 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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.core.blob.binary; 020 021import java.io.BufferedInputStream; 022import java.io.BufferedOutputStream; 023import java.io.DataInputStream; 024import java.io.DataOutputStream; 025import java.io.File; 026import java.io.FileInputStream; 027import java.io.FileOutputStream; 028import java.io.FilterOutputStream; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.OutputStream; 032import java.lang.reflect.Field; 033import java.security.GeneralSecurityException; 034import java.security.Key; 035import java.security.KeyStore; 036import java.security.MessageDigest; 037import java.security.SecureRandom; 038import java.util.Arrays; 039import java.util.Map; 040import java.util.Random; 041 042import javax.crypto.BadPaddingException; 043import javax.crypto.Cipher; 044import javax.crypto.CipherInputStream; 045import javax.crypto.SecretKeyFactory; 046import javax.crypto.spec.IvParameterSpec; 047import javax.crypto.spec.PBEKeySpec; 048import javax.crypto.spec.SecretKeySpec; 049 050import org.apache.commons.io.IOUtils; 051import org.apache.commons.lang.StringUtils; 052import org.apache.commons.logging.Log; 053import org.apache.commons.logging.LogFactory; 054import org.nuxeo.ecm.core.api.NuxeoException; 055import org.nuxeo.runtime.api.Framework; 056 057/** 058 * A binary manager that encrypts binaries on the filesystem using AES. 059 * <p> 060 * The configuration holds the keystore information to retrieve the AES key, or the 061 * password that is used to generate a per-file key using PBKDF2. This configuration comes from the 062 * {@code <property name="key">...</property>} of the binary manager configuration. 063 * <p> 064 * The configuration has the form {@code key1=value1,key2=value2,...} where the possible keys are, for keystore use: 065 * <ul> 066 * <li>keyStoreType: the keystore type, for instance JCEKS 067 * <li>keyStoreFile: the path to the keystore, if applicable 068 * <li>keyStorePassword: the keystore password 069 * <li>keyAlias: the alias (name) of the key in the keystore 070 * <li>keyPassword: the key password 071 * </ul> 072 * <p> 073 * And for PBKDF2 use: 074 * <ul> 075 * <li>password: the password 076 * </ul> 077 * <p> 078 * To encrypt a binary, an AES key is needed. This key can be retrieved from a keystore, or generated from a password 079 * using PBKDF2 (in which case each stored file contains a different salt for security reasons). The file format is 080 * described in {@link #storeAndDigest(InputStream, OutputStream)}. 081 * <p> 082 * While the binary is being used by the application, a temporarily-decrypted file is held in a temporary directory. It 083 * is removed as soon as possible. 084 * <p> 085 * Note: if the Java Cryptographic Extension (JCE) is not configured for 256-bit key length, you may get an exception 086 * "java.security.InvalidKeyException: Illegal key size or default parameters". If this is the case, go to <a 087 * href="http://www.oracle.com/technetwork/java/javase/downloads/index.html" >Oracle Java SE Downloads</a> and download 088 * and install the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for your JDK. 089 * 090 * @since 6.0 091 */ 092public class AESBinaryManager extends LocalBinaryManager { 093 094 private static final Log log = LogFactory.getLog(AESBinaryManager.class); 095 096 protected static final byte[] FILE_MAGIC = new byte[] { 'N', 'U', 'X', 'E', 'O', 'C', 'R', 'Y', 'P', 'T' }; 097 098 protected static final int FILE_VERSION_1 = 1; 099 100 protected static final int USE_KEYSTORE = 1; 101 102 protected static final int USE_PBKDF2 = 2; 103 104 protected static final String AES = "AES"; 105 106 protected static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding"; 107 108 protected static final String PBKDF2_WITH_HMAC_SHA1 = "PBKDF2WithHmacSHA1"; 109 110 protected static final int PBKDF2_ITERATIONS = 10000; 111 112 // AES-256 113 protected static final int PBKDF2_KEY_LENGTH = 256; 114 115 protected static final String PARAM_PASSWORD = "password"; 116 117 protected static final String PARAM_KEY_STORE_TYPE = "keyStoreType"; 118 119 protected static final String PARAM_KEY_STORE_FILE = "keyStoreFile"; 120 121 protected static final String PARAM_KEY_STORE_PASSWORD = "keyStorePassword"; 122 123 protected static final String PARAM_KEY_ALIAS = "keyAlias"; 124 125 protected static final String PARAM_KEY_PASSWORD = "keyPassword"; 126 127 // for sanity check during reads 128 private static final int MAX_SALT_LEN = 1024; 129 130 // for sanity check during reads 131 private static final int MAX_IV_LEN = 1024; 132 133 // Random instances are thread-safe 134 protected static final Random RANDOM = new SecureRandom(); 135 136 // the digest from the root descriptor 137 protected String digestAlgorithm; 138 139 protected boolean usePBKDF2; 140 141 protected String password; 142 143 protected String keyStoreType; 144 145 protected String keyStoreFile; 146 147 protected String keyStorePassword; 148 149 protected String keyAlias; 150 151 protected String keyPassword; 152 153 public AESBinaryManager() { 154 setUnlimitedJCEPolicy(); 155 } 156 157 /** 158 * By default the JRE may ship with restricted key length. Instead of having administrators download the Java 159 * Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files from 160 * http://www.oracle.com/technetwork/java/javase/downloads/index.html, we attempt to directly unrestrict the JCE 161 * using reflection. 162 * <p> 163 * This is not possible anymore since 8u102 and https://bugs.openjdk.java.net/browse/JDK-8149417 164 */ 165 protected static boolean setUnlimitedJCEPolicy() { 166 try { 167 Field field = Class.forName("javax.crypto.JceSecurity").getDeclaredField("isRestricted"); 168 field.setAccessible(true); 169 if (Boolean.TRUE.equals(field.get(null))) { 170 log.info("Setting JCE Unlimited Strength"); 171 field.set(null, Boolean.FALSE); 172 } 173 return true; 174 } catch (ReflectiveOperationException | SecurityException | IllegalArgumentException e) { 175 log.debug("Cannot check/set JCE Unlimited Strength", e); 176 return false; 177 } 178 } 179 180 @Override 181 public void initialize(String blobProviderId, Map<String, String> properties) throws IOException { 182 super.initialize(blobProviderId, properties); 183 digestAlgorithm = getDigestAlgorithm(); 184 String options = properties.get(BinaryManager.PROP_KEY); 185 // TODO parse options from properties directly 186 if (StringUtils.isBlank(options)) { 187 throw new NuxeoException("Missing key for " + getClass().getSimpleName()); 188 } 189 initializeOptions(options); 190 } 191 192 protected void initializeOptions(String options) { 193 for (String option : options.split(",")) { 194 String[] split = option.split("=", 2); 195 if (split.length != 2) { 196 throw new NuxeoException("Unrecognized option: " + option); 197 } 198 String value = StringUtils.defaultIfBlank(split[1], null); 199 switch (split[0]) { 200 case PARAM_PASSWORD: 201 password = value; 202 break; 203 case PARAM_KEY_STORE_TYPE: 204 keyStoreType = value; 205 break; 206 case PARAM_KEY_STORE_FILE: 207 keyStoreFile = value; 208 break; 209 case PARAM_KEY_STORE_PASSWORD: 210 keyStorePassword = value; 211 break; 212 case PARAM_KEY_ALIAS: 213 keyAlias = value; 214 break; 215 case PARAM_KEY_PASSWORD: 216 keyPassword = value; 217 break; 218 default: 219 throw new NuxeoException("Unrecognized option: " + option); 220 } 221 } 222 usePBKDF2 = password != null; 223 if (usePBKDF2) { 224 if (keyStoreType != null) { 225 throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_TYPE + " with " + PARAM_PASSWORD); 226 } 227 if (keyStoreFile != null) { 228 throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_FILE + " with " + PARAM_PASSWORD); 229 } 230 if (keyStorePassword != null) { 231 throw new NuxeoException("Cannot use " + PARAM_KEY_STORE_PASSWORD + " with " + PARAM_PASSWORD); 232 } 233 if (keyAlias != null) { 234 throw new NuxeoException("Cannot use " + PARAM_KEY_ALIAS + " with " + PARAM_PASSWORD); 235 } 236 if (keyPassword != null) { 237 throw new NuxeoException("Cannot use " + PARAM_KEY_PASSWORD + " with " + PARAM_PASSWORD); 238 } 239 } else { 240 if (keyStoreType == null) { 241 throw new NuxeoException("Missing " + PARAM_KEY_STORE_TYPE); 242 } 243 // keystore file is optional 244 if (keyStoreFile == null && keyStorePassword != null) { 245 throw new NuxeoException("Missing " + PARAM_KEY_STORE_PASSWORD); 246 } 247 if (keyAlias == null) { 248 throw new NuxeoException("Missing " + PARAM_KEY_ALIAS); 249 } 250 if (keyPassword == null) { 251 keyPassword = keyStorePassword; 252 } 253 } 254 } 255 256 /** 257 * Gets the password for PBKDF2. 258 * <p> 259 * The caller must clear it from memory when done with it by calling {@link #clearPassword}. 260 */ 261 protected char[] getPassword() { 262 return password.toCharArray(); 263 } 264 265 /** 266 * Clears a password from memory. 267 */ 268 protected void clearPassword(char[] password) { 269 if (password != null) { 270 Arrays.fill(password, '\0'); 271 } 272 } 273 274 /** 275 * Generates an AES key from the password using PBKDF2. 276 * 277 * @param salt the salt 278 */ 279 protected Key generateSecretKey(byte[] salt) throws GeneralSecurityException { 280 char[] password = getPassword(); 281 SecretKeyFactory factory = SecretKeyFactory.getInstance(PBKDF2_WITH_HMAC_SHA1); 282 PBEKeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH); 283 clearPassword(password); 284 Key derived = factory.generateSecret(spec); 285 spec.clearPassword(); 286 return new SecretKeySpec(derived.getEncoded(), AES); 287 } 288 289 /** 290 * Gets the AES key from the keystore. 291 */ 292 protected Key getSecretKey() throws GeneralSecurityException, IOException { 293 KeyStore keyStore = KeyStore.getInstance(keyStoreType); 294 char[] kspw = keyStorePassword == null ? null : keyStorePassword.toCharArray(); 295 if (keyStoreFile != null) { 296 try (InputStream in = new BufferedInputStream(new FileInputStream(keyStoreFile))) { 297 keyStore.load(in, kspw); 298 } 299 } else { 300 // some keystores are not backed by a file 301 keyStore.load(null, kspw); 302 } 303 clearPassword(kspw); 304 char[] kpw = keyPassword == null ? null : keyPassword.toCharArray(); 305 Key key = keyStore.getKey(keyAlias, kpw); 306 clearPassword(kpw); 307 return key; 308 } 309 310 @Override 311 protected Binary getBinary(InputStream in) throws IOException { 312 // write to a tmp file that will be used by the returned Binary 313 // TODO if stream source, avoid copy (no-copy optimization) 314 File tmp = File.createTempFile("bin_", ".tmp", tmpDir); 315 Framework.trackFile(tmp, tmp); 316 OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); 317 IOUtils.copy(in, out); 318 in.close(); 319 out.close(); 320 // encrypt an digest into final file 321 InputStream nin = new BufferedInputStream(new FileInputStream(tmp)); 322 String digest = storeAndDigest(nin); // calls our storeAndDigest 323 // return a binary on our tmp file 324 return new Binary(tmp, digest, blobProviderId); 325 } 326 327 @Override 328 public Binary getBinary(String digest) { 329 File file = getFileForDigest(digest, false); 330 if (file == null) { 331 log.warn("Invalid digest format: " + digest); 332 return null; 333 } 334 if (!file.exists()) { 335 return null; 336 } 337 File tmp; 338 try { 339 tmp = File.createTempFile("bin_", ".tmp", tmpDir); 340 Framework.trackFile(tmp, tmp); 341 OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); 342 InputStream in = new BufferedInputStream(new FileInputStream(file)); 343 try { 344 decrypt(in, out); 345 } finally { 346 in.close(); 347 out.close(); 348 } 349 } catch (IOException e) { 350 throw new RuntimeException(e); 351 } 352 // return a binary on our tmp file 353 return new Binary(tmp, digest, blobProviderId); 354 } 355 356 @Override 357 protected String storeAndDigest(InputStream in) throws IOException { 358 File tmp = File.createTempFile("create_", ".tmp", tmpDir); 359 OutputStream out = new BufferedOutputStream(new FileOutputStream(tmp)); 360 /* 361 * First, write the input stream to a temporary file, while computing a digest. 362 */ 363 try { 364 String digest; 365 try { 366 digest = storeAndDigest(in, out); 367 } finally { 368 in.close(); 369 out.close(); 370 } 371 /* 372 * Move the tmp file to its destination. 373 */ 374 File file = getFileForDigest(digest, true); 375 atomicMove(tmp, file); 376 return digest; 377 } finally { 378 tmp.delete(); 379 } 380 } 381 382 /** 383 * Encrypts the given input stream into the given output stream, while also computing the digest of the input 384 * stream. 385 * <p> 386 * File format version 1 (values are in network order): 387 * <ul> 388 * <li>10 bytes: magic number "NUXEOCRYPT" 389 * <li>1 byte: file format version = 1 390 * <li>1 byte: use keystore = 1, use PBKDF2 = 2 391 * <li>if use PBKDF2: 392 * <ul> 393 * <li>4 bytes: salt length = n 394 * <li>n bytes: salt data 395 * </ul> 396 * <li>4 bytes: IV length = p 397 * <li>p bytes: IV data 398 * <li>x bytes: encrypted stream 399 * </ul> 400 * 401 * @param in the input stream containing the data 402 * @param file the file containing the encrypted data 403 * @return the digest of the input stream 404 */ 405 @Override 406 public String storeAndDigest(InputStream in, OutputStream out) throws IOException { 407 out.write(FILE_MAGIC); 408 DataOutputStream data = new DataOutputStream(out); 409 data.writeByte(FILE_VERSION_1); 410 411 try { 412 // get digest to use 413 MessageDigest messageDigest = MessageDigest.getInstance(digestAlgorithm); 414 415 // secret key 416 Key secret; 417 if (usePBKDF2) { 418 data.writeByte(USE_PBKDF2); 419 // generate a salt 420 byte[] salt = new byte[16]; 421 RANDOM.nextBytes(salt); 422 // generate secret key 423 secret = generateSecretKey(salt); 424 // write salt 425 data.writeInt(salt.length); 426 data.write(salt); 427 } else { 428 data.writeByte(USE_KEYSTORE); 429 // find secret key from keystore 430 secret = getSecretKey(); 431 } 432 433 // cipher 434 Cipher cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING); 435 cipher.init(Cipher.ENCRYPT_MODE, secret); 436 437 // write IV 438 byte[] iv = cipher.getIV(); 439 data.writeInt(iv.length); 440 data.write(iv); 441 442 // digest and write the encrypted data 443 CipherAndDigestOutputStream cipherOut = new CipherAndDigestOutputStream(out, cipher, messageDigest); 444 IOUtils.copy(in, cipherOut); 445 cipherOut.close(); 446 byte[] digest = cipherOut.getDigest(); 447 return toHexString(digest); 448 } catch (GeneralSecurityException e) { 449 throw new NuxeoException(e); 450 } 451 452 } 453 454 /** 455 * Decrypts the given input stream into the given output stream. 456 */ 457 protected void decrypt(InputStream in, OutputStream out) throws IOException { 458 byte[] magic = new byte[FILE_MAGIC.length]; 459 IOUtils.read(in, magic); 460 if (!Arrays.equals(magic, FILE_MAGIC)) { 461 throw new IOException("Invalid file (bad magic)"); 462 } 463 DataInputStream data = new DataInputStream(in); 464 byte magicvers = data.readByte(); 465 if (magicvers != FILE_VERSION_1) { 466 throw new IOException("Invalid file (bad version)"); 467 } 468 469 byte usepb = data.readByte(); 470 if (usepb == USE_PBKDF2) { 471 if (!usePBKDF2) { 472 throw new NuxeoException("File requires PBKDF2 password"); 473 } 474 } else if (usepb == USE_KEYSTORE) { 475 if (usePBKDF2) { 476 throw new NuxeoException("File requires keystore"); 477 } 478 } else { 479 throw new IOException("Invalid file (bad use)"); 480 } 481 482 try { 483 // secret key 484 Key secret; 485 if (usePBKDF2) { 486 // read salt first 487 int saltLen = data.readInt(); 488 if (saltLen <= 0 || saltLen > MAX_SALT_LEN) { 489 throw new NuxeoException("Invalid salt length: " + saltLen); 490 } 491 byte[] salt = new byte[saltLen]; 492 data.read(salt, 0, saltLen); 493 secret = generateSecretKey(salt); 494 } else { 495 secret = getSecretKey(); 496 } 497 498 // read IV 499 int ivLen = data.readInt(); 500 if (ivLen <= 0 || ivLen > MAX_IV_LEN) { 501 throw new NuxeoException("Invalid IV length: " + ivLen); 502 } 503 byte[] iv = new byte[ivLen]; 504 data.read(iv, 0, ivLen); 505 506 // cipher 507 Cipher cipher; 508 cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING); 509 cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv)); 510 511 // read the encrypted data 512 try (InputStream cipherIn = new CipherInputStream(in, cipher)) { 513 IOUtils.copy(cipherIn, out); 514 } catch (IOException e) { 515 Throwable cause = e.getCause(); 516 if (cause != null && cause instanceof BadPaddingException) { 517 throw new NuxeoException(cause.getMessage(), e); 518 } 519 } 520 } catch (GeneralSecurityException e) { 521 throw new NuxeoException(e); 522 } 523 } 524 525 /** 526 * A {@link javax.crypto.CipherOutputStream CipherOutputStream} that also does a digest of the original stream at 527 * the same time. 528 */ 529 public static class CipherAndDigestOutputStream extends FilterOutputStream { 530 531 protected Cipher cipher; 532 533 protected OutputStream out; 534 535 protected MessageDigest messageDigest; 536 537 protected byte[] digest; 538 539 public CipherAndDigestOutputStream(OutputStream out, Cipher cipher, MessageDigest messageDigest) { 540 super(out); 541 this.out = out; 542 this.cipher = cipher; 543 this.messageDigest = messageDigest; 544 } 545 546 public byte[] getDigest() { 547 return digest; 548 } 549 550 @Override 551 public void write(int b) throws IOException { 552 write(new byte[] { (byte) b }, 0, 1); 553 } 554 555 @Override 556 public void write(byte b[], int off, int len) throws IOException { 557 messageDigest.update(b, off, len); 558 byte[] bytes = cipher.update(b, off, len); 559 if (bytes != null) { 560 out.write(bytes); 561 bytes = null; // help GC 562 } 563 } 564 565 @Override 566 public void flush() throws IOException { 567 out.flush(); 568 } 569 570 @Override 571 public void close() throws IOException { 572 digest = messageDigest.digest(); 573 try { 574 byte[] bytes = cipher.doFinal(); 575 out.write(bytes); 576 bytes = null; // help GC 577 } catch (GeneralSecurityException e) { 578 throw new NuxeoException(e); 579 } 580 try { 581 flush(); 582 } finally { 583 out.close(); 584 } 585 } 586 } 587 588}