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