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