|
@@ -1,798 +0,0 @@
|
|
|
-/*******************************************************************************
|
|
|
- * Copyright (c) 2014 Sebastian Stenzel
|
|
|
- * This file is licensed under the terms of the MIT license.
|
|
|
- * See the LICENSE.txt file for more info.
|
|
|
- *
|
|
|
- * Contributors:
|
|
|
- * Sebastian Stenzel - initial API and implementation
|
|
|
- ******************************************************************************/
|
|
|
-package org.cryptomator.crypto.aes256;
|
|
|
-
|
|
|
-import java.io.IOException;
|
|
|
-import java.io.InputStream;
|
|
|
-import java.io.OutputStream;
|
|
|
-import java.nio.ByteBuffer;
|
|
|
-import java.nio.channels.Channels;
|
|
|
-import java.nio.channels.ReadableByteChannel;
|
|
|
-import java.nio.channels.SeekableByteChannel;
|
|
|
-import java.nio.charset.StandardCharsets;
|
|
|
-import java.security.InvalidAlgorithmParameterException;
|
|
|
-import java.security.InvalidKeyException;
|
|
|
-import java.security.MessageDigest;
|
|
|
-import java.security.NoSuchAlgorithmException;
|
|
|
-import java.security.SecureRandom;
|
|
|
-import java.util.Arrays;
|
|
|
-import java.util.concurrent.ExecutionException;
|
|
|
-import java.util.concurrent.TimeUnit;
|
|
|
-import java.util.concurrent.TimeoutException;
|
|
|
-
|
|
|
-import javax.crypto.AEADBadTagException;
|
|
|
-import javax.crypto.BadPaddingException;
|
|
|
-import javax.crypto.Cipher;
|
|
|
-import javax.crypto.IllegalBlockSizeException;
|
|
|
-import javax.crypto.Mac;
|
|
|
-import javax.crypto.NoSuchPaddingException;
|
|
|
-import javax.crypto.SecretKey;
|
|
|
-import javax.crypto.ShortBufferException;
|
|
|
-import javax.crypto.spec.IvParameterSpec;
|
|
|
-import javax.crypto.spec.SecretKeySpec;
|
|
|
-import javax.security.auth.DestroyFailedException;
|
|
|
-import javax.security.auth.Destroyable;
|
|
|
-
|
|
|
-import org.bouncycastle.crypto.generators.SCrypt;
|
|
|
-import org.cryptomator.crypto.Cryptor;
|
|
|
-import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
|
|
-import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
|
|
-import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
|
|
-import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
|
|
-import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
|
|
-import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
|
|
-import org.cryptomator.siv.SivMode;
|
|
|
-import org.slf4j.Logger;
|
|
|
-import org.slf4j.LoggerFactory;
|
|
|
-
|
|
|
-import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
-
|
|
|
-public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|
|
-
|
|
|
- private static final Logger LOG = LoggerFactory.getLogger(Aes256Cryptor.class);
|
|
|
-
|
|
|
- /**
|
|
|
- * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
|
|
|
- * here: http://www.oracle.com/technetwork/java/javase/downloads/.
|
|
|
- */
|
|
|
- private static final int AES_KEY_LENGTH_IN_BITS;
|
|
|
-
|
|
|
- /**
|
|
|
- * SIV mode for deterministic filename encryption.
|
|
|
- */
|
|
|
- private static final SivMode AES_SIV = new SivMode();
|
|
|
-
|
|
|
- /**
|
|
|
- * PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
|
|
|
- *
|
|
|
- * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
|
|
|
- */
|
|
|
- private final SecureRandom securePrng;
|
|
|
-
|
|
|
- /**
|
|
|
- * Jackson JSON-Mapper.
|
|
|
- */
|
|
|
- private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
-
|
|
|
- /**
|
|
|
- * The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with
|
|
|
- * {@link #swipeSensitiveData()}.
|
|
|
- */
|
|
|
- private SecretKey primaryMasterKey;
|
|
|
-
|
|
|
- /**
|
|
|
- * Decrypted secondary key used for hmac operations.
|
|
|
- */
|
|
|
- private SecretKey hMacMasterKey;
|
|
|
-
|
|
|
- static {
|
|
|
- try {
|
|
|
- final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
|
|
|
- AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS) ? PREF_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
|
|
|
- } catch (NoSuchAlgorithmException e) {
|
|
|
- throw new IllegalStateException("Algorithm should exist.", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Creates a new Cryptor with a newly initialized PRNG.
|
|
|
- */
|
|
|
-
|
|
|
- Aes256Cryptor(SecureRandom securePrng) {
|
|
|
- this.securePrng = securePrng;
|
|
|
- // No setSeed needed. See SecureRandom.getInstance(String):
|
|
|
- // The first call to nextBytes will force the SecureRandom object to seed itself
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void randomizeMasterKey() {
|
|
|
- byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
|
|
|
- try {
|
|
|
- // No setSeed needed. See SecureRandom.getInstance(String):
|
|
|
- // The first call to nextBytes will force the SecureRandom object to seed itself
|
|
|
- securePrng.nextBytes(bytes);
|
|
|
- this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
|
|
|
- securePrng.nextBytes(bytes);
|
|
|
- this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
|
|
|
- } finally {
|
|
|
- Arrays.fill(bytes, (byte) 0);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
|
|
- try {
|
|
|
- // derive key:
|
|
|
- final byte[] kekSalt = randomData(SCRYPT_SALT_LENGTH);
|
|
|
- final SecretKey kek = scrypt(password, kekSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, AES_KEY_LENGTH_IN_BITS);
|
|
|
-
|
|
|
- // encrypt:
|
|
|
- final Cipher encCipher = aesKeyWrapCipher(kek, Cipher.WRAP_MODE);
|
|
|
- byte[] wrappedPrimaryKey = encCipher.wrap(primaryMasterKey);
|
|
|
- byte[] wrappedSecondaryKey = encCipher.wrap(hMacMasterKey);
|
|
|
-
|
|
|
- // save encrypted masterkey:
|
|
|
- final KeyFile keyfile = new KeyFile();
|
|
|
- keyfile.setVersion(KeyFile.CURRENT_VERSION);
|
|
|
- keyfile.setScryptSalt(kekSalt);
|
|
|
- keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
|
|
|
- keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
|
|
|
- keyfile.setKeyLength(AES_KEY_LENGTH_IN_BITS);
|
|
|
- keyfile.setPrimaryMasterKey(wrappedPrimaryKey);
|
|
|
- keyfile.setHMacMasterKey(wrappedSecondaryKey);
|
|
|
- objectMapper.writeValue(out, keyfile);
|
|
|
- } catch (InvalidKeyException | IllegalBlockSizeException ex) {
|
|
|
- throw new IllegalStateException("Invalid hard coded configuration.", ex);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
|
|
|
- try {
|
|
|
- // load encrypted masterkey:
|
|
|
- final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
|
|
|
-
|
|
|
- // check version
|
|
|
- if (keyfile.getVersion() != KeyFile.CURRENT_VERSION) {
|
|
|
- throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION);
|
|
|
- }
|
|
|
-
|
|
|
- // check, whether the key length is supported:
|
|
|
- final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
|
|
|
- if (keyfile.getKeyLength() > maxKeyLen) {
|
|
|
- throw new UnsupportedKeyLengthException(keyfile.getKeyLength(), maxKeyLen);
|
|
|
- }
|
|
|
-
|
|
|
- // derive key:
|
|
|
- final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), keyfile.getKeyLength());
|
|
|
-
|
|
|
- // decrypt and check password by catching AEAD exception
|
|
|
- final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
|
|
|
- SecretKey primary = (SecretKey) decCipher.unwrap(keyfile.getPrimaryMasterKey(), AES_KEY_ALGORITHM, Cipher.SECRET_KEY);
|
|
|
- SecretKey secondary = (SecretKey) decCipher.unwrap(keyfile.getHMacMasterKey(), HMAC_KEY_ALGORITHM, Cipher.SECRET_KEY);
|
|
|
-
|
|
|
- // everything ok, assign decrypted keys:
|
|
|
- this.primaryMasterKey = primary;
|
|
|
- this.hMacMasterKey = secondary;
|
|
|
- } catch (NoSuchAlgorithmException ex) {
|
|
|
- throw new IllegalStateException("Algorithm should exist.", ex);
|
|
|
- } catch (InvalidKeyException e) {
|
|
|
- throw new WrongPasswordException();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public boolean isDestroyed() {
|
|
|
- if (primaryMasterKey == null || hMacMasterKey == null) {
|
|
|
- // master keys have not been set yet, so there is nothing sensitive
|
|
|
- return true;
|
|
|
- }
|
|
|
- return primaryMasterKey.isDestroyed() && hMacMasterKey.isDestroyed();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public void destroy() {
|
|
|
- destroyQuietly(primaryMasterKey);
|
|
|
- destroyQuietly(hMacMasterKey);
|
|
|
- }
|
|
|
-
|
|
|
- private void destroyQuietly(Destroyable d) {
|
|
|
- try {
|
|
|
- d.destroy();
|
|
|
- } catch (DestroyFailedException e) {
|
|
|
- // ignore
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Cipher aesKeyWrapCipher(SecretKey key, int cipherMode) {
|
|
|
- try {
|
|
|
- final Cipher cipher = Cipher.getInstance(AES_KEYWRAP_CIPHER);
|
|
|
- cipher.init(cipherMode, key);
|
|
|
- return cipher;
|
|
|
- } catch (InvalidKeyException ex) {
|
|
|
- throw new IllegalArgumentException("Invalid key.", ex);
|
|
|
- } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
|
|
- throw new IllegalStateException("Algorithm/Padding should exist.", ex);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Cipher aesCtrCipher(SecretKey key, byte[] iv, int cipherMode) {
|
|
|
- try {
|
|
|
- final Cipher cipher = Cipher.getInstance(AES_CTR_CIPHER);
|
|
|
- cipher.init(cipherMode, key, new IvParameterSpec(iv));
|
|
|
- return cipher;
|
|
|
- } catch (InvalidKeyException ex) {
|
|
|
- throw new IllegalArgumentException("Invalid key.", ex);
|
|
|
- } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
|
|
|
- throw new IllegalStateException("Algorithm/Padding should exist and accept an IV.", ex);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Cipher aesCbcCipher(SecretKey key, byte[] iv, int cipherMode) {
|
|
|
- try {
|
|
|
- final Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER);
|
|
|
- cipher.init(cipherMode, key, new IvParameterSpec(iv));
|
|
|
- return cipher;
|
|
|
- } catch (InvalidKeyException ex) {
|
|
|
- throw new IllegalArgumentException("Invalid key.", ex);
|
|
|
- } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
|
|
|
- throw new AssertionError("Every implementation of the Java platform is required to support AES/CBC/PKCS5Padding, which accepts an IV", ex);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Mac hmacSha256(SecretKey key) {
|
|
|
- try {
|
|
|
- final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
|
|
|
- mac.init(key);
|
|
|
- return mac;
|
|
|
- } catch (NoSuchAlgorithmException e) {
|
|
|
- throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
|
|
|
- } catch (InvalidKeyException e) {
|
|
|
- throw new IllegalArgumentException("Invalid key", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private MessageDigest sha256() {
|
|
|
- try {
|
|
|
- return MessageDigest.getInstance("SHA-256");
|
|
|
- } catch (NoSuchAlgorithmException e) {
|
|
|
- throw new AssertionError("Every implementation of the Java platform is required to support Sha-256");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private byte[] randomData(int length) {
|
|
|
- final byte[] result = new byte[length];
|
|
|
- securePrng.nextBytes(result);
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
- private SecretKey scrypt(CharSequence password, byte[] salt, int costParam, int blockSize, int keyLengthInBits) {
|
|
|
- // use sb, as password.toString's implementation is unknown
|
|
|
- final StringBuilder sb = new StringBuilder(password);
|
|
|
- final byte[] pw = sb.toString().getBytes();
|
|
|
- try {
|
|
|
- final byte[] key = SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBits / Byte.SIZE);
|
|
|
- return new SecretKeySpec(key, AES_KEY_ALGORITHM);
|
|
|
- } finally {
|
|
|
- // destroy copied bytes of the plaintext password:
|
|
|
- Arrays.fill(pw, (byte) 0);
|
|
|
- for (int i = 0; i < password.length(); i++) {
|
|
|
- sb.setCharAt(i, (char) 0);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
|
|
- final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
|
|
|
- byte[] encryptedBytes = AES_SIV.encrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
|
|
|
- final byte[] hashed = sha256().digest(encryptedBytes);
|
|
|
- final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed);
|
|
|
- return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public String encryptFilename(String cleartextName) {
|
|
|
- final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
|
|
|
- final byte[] encryptedBytes = AES_SIV.encrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
|
|
|
- return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
|
|
- final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertextName);
|
|
|
- try {
|
|
|
- final byte[] cleartextBytes = AES_SIV.decrypt(primaryMasterKey, hMacMasterKey, encryptedBytes);
|
|
|
- return new String(cleartextBytes, StandardCharsets.UTF_8);
|
|
|
- } catch (AEADBadTagException e) {
|
|
|
- throw new DecryptFailedException(e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
|
|
- // read header:
|
|
|
- encryptedFile.position(0);
|
|
|
- final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
|
|
- final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
|
|
- if (headerBytesRead != headerBuf.capacity()) {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- // read iv:
|
|
|
- final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
|
|
- headerBuf.position(0);
|
|
|
- headerBuf.get(iv);
|
|
|
-
|
|
|
- // read sensitive header data:
|
|
|
- final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
|
|
- headerBuf.position(24);
|
|
|
- headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
|
|
-
|
|
|
- // read stored header mac:
|
|
|
- final byte[] storedHeaderMac = new byte[32];
|
|
|
- headerBuf.position(72);
|
|
|
- headerBuf.get(storedHeaderMac);
|
|
|
-
|
|
|
- // calculate mac over first 72 bytes of header:
|
|
|
- final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
|
|
- headerBuf.rewind();
|
|
|
- headerBuf.limit(72);
|
|
|
- headerMac.update(headerBuf);
|
|
|
-
|
|
|
- final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
|
|
- if (!macMatches) {
|
|
|
- throw new MacAuthenticationFailedException("MAC authentication failed.");
|
|
|
- }
|
|
|
-
|
|
|
- // decrypt sensitive header data:
|
|
|
- final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
|
|
- final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
|
|
- final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
|
|
-
|
|
|
- return fileSize;
|
|
|
- }
|
|
|
-
|
|
|
- private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
|
|
|
- try {
|
|
|
- final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
|
|
- return sizeCipher.doFinal(ciphertextBytes);
|
|
|
- } catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
|
- throw new IllegalStateException(e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
|
|
|
- try {
|
|
|
- final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
|
|
- return sizeCipher.doFinal(plaintextBytes);
|
|
|
- } catch (IllegalBlockSizeException | BadPaddingException e) {
|
|
|
- throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
|
|
- // read header:
|
|
|
- encryptedFile.position(0l);
|
|
|
- final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
|
|
- final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
|
|
- if (headerBytesRead != headerBuf.capacity()) {
|
|
|
- throw new IOException("Failed to read file header.");
|
|
|
- }
|
|
|
-
|
|
|
- // read iv:
|
|
|
- final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
|
|
- headerBuf.position(0);
|
|
|
- headerBuf.get(iv);
|
|
|
-
|
|
|
- // read nonce:
|
|
|
- final byte[] nonce = new byte[8];
|
|
|
- headerBuf.position(16);
|
|
|
- headerBuf.get(nonce);
|
|
|
-
|
|
|
- // read sensitive header data:
|
|
|
- final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
|
|
- headerBuf.position(24);
|
|
|
- headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
|
|
-
|
|
|
- // read header mac:
|
|
|
- final byte[] storedHeaderMac = new byte[32];
|
|
|
- headerBuf.position(72);
|
|
|
- headerBuf.get(storedHeaderMac);
|
|
|
-
|
|
|
- // calculate mac over first 72 bytes of header:
|
|
|
- if (authenticate) {
|
|
|
- final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
|
|
- headerBuf.position(0);
|
|
|
- headerBuf.limit(72);
|
|
|
- headerMac.update(headerBuf);
|
|
|
- if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
|
|
- throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // decrypt sensitive header data:
|
|
|
- final byte[] fileKeyBytes = new byte[32];
|
|
|
- final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
|
|
- final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
|
|
- final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
|
|
- sensitiveHeaderContentBuf.get(fileKeyBytes);
|
|
|
-
|
|
|
- // prepare content decryption:
|
|
|
- final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
|
|
- final LengthLimitingOutputStream paddingRemovingOutputStream = new LengthLimitingOutputStream(plaintextFile, fileSize);
|
|
|
- final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
|
|
- return new DecryptWorker(lock, blockDone, currentBlock, inputQueue, authenticate, Channels.newChannel(paddingRemovingOutputStream)) {
|
|
|
-
|
|
|
- @Override
|
|
|
- protected Cipher initCipher(long startBlockNum) {
|
|
|
- final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
|
|
- nonceAndCounterBuf.put(nonce);
|
|
|
- nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
|
|
- final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
|
|
- return aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected Mac initMac() {
|
|
|
- return hmacSha256(hMacMasterKey);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException {
|
|
|
- mac.update(iv);
|
|
|
- mac.update(longToByteArray(blockNum));
|
|
|
- mac.update(ciphertextBuf);
|
|
|
- final byte[] calculatedMac = mac.doFinal();
|
|
|
- final byte[] storedMac = new byte[mac.getMacLength()];
|
|
|
- macBuf.get(storedMac);
|
|
|
- if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
|
|
- throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException {
|
|
|
- assert plaintextBuf.remaining() >= cipher.getOutputSize(ciphertextBuf.remaining());
|
|
|
- try {
|
|
|
- cipher.update(ciphertextBuf, plaintextBuf);
|
|
|
- } catch (ShortBufferException e) {
|
|
|
- throw new DecryptFailedException(e);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- // read as many blocks from file as possible, but wait if queue is full:
|
|
|
- encryptedFile.position(104l);
|
|
|
- final int maxNumBlocks = 64;
|
|
|
- int numBlocks = 1;
|
|
|
- int bytesRead = 0;
|
|
|
- long blockNumber = 0;
|
|
|
- do {
|
|
|
- if (numBlocks < maxNumBlocks) {
|
|
|
- numBlocks++;
|
|
|
- }
|
|
|
- final int inBufSize = numBlocks * (CONTENT_MAC_BLOCK + 32);
|
|
|
- final ByteBuffer buf = ByteBuffer.allocate(inBufSize);
|
|
|
- bytesRead = readFromChannel(encryptedFile, buf);
|
|
|
- buf.flip();
|
|
|
- final int blocksRead = (int) Math.ceil(bytesRead / (double) (CONTENT_MAC_BLOCK + 32));
|
|
|
- final boolean consumedInTime = executor.offer(new BlocksData(buf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
|
|
- if (!consumedInTime) {
|
|
|
- break;
|
|
|
- }
|
|
|
- blockNumber += numBlocks;
|
|
|
- } while (bytesRead == numBlocks * (CONTENT_MAC_BLOCK + 32));
|
|
|
-
|
|
|
- // wait for decryption workers to finish:
|
|
|
- try {
|
|
|
- executor.waitUntilDone(1, TimeUnit.SECONDS);
|
|
|
- } catch (ExecutionException e) {
|
|
|
- Throwable cause = e;
|
|
|
- while (cause instanceof ExecutionException) {
|
|
|
- cause = cause.getCause();
|
|
|
- }
|
|
|
- if (cause instanceof IOException) {
|
|
|
- throw (IOException) cause;
|
|
|
- } else if (cause instanceof TimeoutException) {
|
|
|
- throw new DecryptFailedException(cause);
|
|
|
- } else if (cause instanceof RuntimeException) {
|
|
|
- throw (RuntimeException) cause;
|
|
|
- } else {
|
|
|
- LOG.error("Unexpected exception", e);
|
|
|
- throw new RuntimeException(cause);
|
|
|
- }
|
|
|
- } finally {
|
|
|
- destroyQuietly(fileKey);
|
|
|
- }
|
|
|
-
|
|
|
- return paddingRemovingOutputStream.getBytesWritten();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
|
|
- // read header:
|
|
|
- encryptedFile.position(0l);
|
|
|
- final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
|
|
- final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
|
|
- if (headerBytesRead != headerBuf.capacity()) {
|
|
|
- throw new IOException("Failed to read file header.");
|
|
|
- }
|
|
|
-
|
|
|
- // read iv:
|
|
|
- final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
|
|
- headerBuf.position(0);
|
|
|
- headerBuf.get(iv);
|
|
|
-
|
|
|
- // read nonce:
|
|
|
- final byte[] nonce = new byte[8];
|
|
|
- headerBuf.position(16);
|
|
|
- headerBuf.get(nonce);
|
|
|
-
|
|
|
- // read sensitive header data:
|
|
|
- final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
|
|
- headerBuf.position(24);
|
|
|
- headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
|
|
-
|
|
|
- // read header mac:
|
|
|
- final byte[] storedHeaderMac = new byte[32];
|
|
|
- headerBuf.position(72);
|
|
|
- headerBuf.get(storedHeaderMac);
|
|
|
-
|
|
|
- // calculate mac over first 72 bytes of header:
|
|
|
- if (authenticate) {
|
|
|
- final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
|
|
- headerBuf.position(0);
|
|
|
- headerBuf.limit(72);
|
|
|
- headerMac.update(headerBuf);
|
|
|
- if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
|
|
- throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // decrypt sensitive header data:
|
|
|
- final byte[] fileKeyBytes = new byte[32];
|
|
|
- final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
|
|
- final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
|
|
- final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
|
|
- sensitiveHeaderContentBuf.get(fileKeyBytes);
|
|
|
-
|
|
|
- assert pos + length - 1 < fileSize;
|
|
|
-
|
|
|
- // find first relevant block:
|
|
|
- final long startBlock = pos / CONTENT_MAC_BLOCK; // floor
|
|
|
- final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104;
|
|
|
- final long offsetFromFirstBlock = pos - startBlock * CONTENT_MAC_BLOCK;
|
|
|
-
|
|
|
- // append correct counter value to nonce:
|
|
|
- final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
|
|
- nonceAndCounterBuf.put(nonce);
|
|
|
- nonceAndCounterBuf.putLong(startBlock * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
|
|
- final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
|
|
-
|
|
|
- // content decryption:
|
|
|
- encryptedFile.position(startByte);
|
|
|
- final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
|
|
- final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
|
|
- final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
|
|
-
|
|
|
- try {
|
|
|
- // reading ciphered input and MACs interleaved:
|
|
|
- long bytesWritten = 0;
|
|
|
- final ByteBuffer buf = ByteBuffer.allocate(CONTENT_MAC_BLOCK + 32);
|
|
|
- int n = 0;
|
|
|
- long blockNum = startBlock;
|
|
|
- while ((n = readFromChannel(encryptedFile, buf)) > 0 && bytesWritten < length) {
|
|
|
- if (n < 32) {
|
|
|
- throw new DecryptFailedException("Invalid file content, missing MAC.");
|
|
|
- }
|
|
|
-
|
|
|
- buf.flip();
|
|
|
- final ByteBuffer ciphertextBuf = buf.asReadOnlyBuffer();
|
|
|
- ciphertextBuf.limit(n - 32);
|
|
|
-
|
|
|
- // check MAC of current block:
|
|
|
- if (authenticate) {
|
|
|
- final byte[] storedMac = new byte[contentMac.getMacLength()];
|
|
|
- final ByteBuffer storedMacBuf = buf.asReadOnlyBuffer();
|
|
|
- storedMacBuf.position(n - 32);
|
|
|
- storedMacBuf.get(storedMac);
|
|
|
- contentMac.update(iv);
|
|
|
- contentMac.update(longToByteArray(blockNum));
|
|
|
- contentMac.update(ciphertextBuf);
|
|
|
- ciphertextBuf.rewind();
|
|
|
- final byte[] calculatedMac = contentMac.doFinal();
|
|
|
- if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
|
|
- throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // decrypt block:
|
|
|
- final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(ciphertextBuf.remaining()));
|
|
|
- cipher.update(ciphertextBuf, plaintextBuf);
|
|
|
- plaintextBuf.flip();
|
|
|
- final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
|
|
|
- final long pending = length - bytesWritten;
|
|
|
- final int available = plaintextBuf.remaining() - offset;
|
|
|
- final int currentBatch = (int) Math.min(pending, available);
|
|
|
-
|
|
|
- plaintextFile.write(plaintextBuf.array(), offset, currentBatch);
|
|
|
- bytesWritten += currentBatch;
|
|
|
- blockNum++;
|
|
|
- buf.rewind();
|
|
|
- }
|
|
|
- return bytesWritten;
|
|
|
- } catch (ShortBufferException e) {
|
|
|
- throw new IllegalStateException("Output buffer size known to fit.", e);
|
|
|
- } finally {
|
|
|
- destroyQuietly(fileKey);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * header = {16 byte iv, 8 byte nonce, 48 byte sensitive header data (file size + file key + padding), 32 byte headerMac}
|
|
|
- */
|
|
|
- @Override
|
|
|
- public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
|
|
- // truncate file
|
|
|
- encryptedFile.truncate(0l);
|
|
|
-
|
|
|
- // choose a random header IV:
|
|
|
- final byte[] iv = randomData(AES_BLOCK_LENGTH);
|
|
|
-
|
|
|
- // chosse 8 byte random nonce and 8 byte counter set to zero:
|
|
|
- final byte[] nonce = randomData(8);
|
|
|
-
|
|
|
- // choose a random content key:
|
|
|
- final byte[] fileKeyBytes = randomData(32);
|
|
|
-
|
|
|
- // 104 byte header buffer (16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac), filled after writing the content
|
|
|
- final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
|
|
- headerBuf.limit(104);
|
|
|
- encryptedFile.write(headerBuf);
|
|
|
-
|
|
|
- // prepare content encryption:
|
|
|
- final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
|
|
- final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
|
|
- return new EncryptWorker(lock, blockDone, currentBlock, inputQueue, encryptedFile) {
|
|
|
-
|
|
|
- @Override
|
|
|
- protected Cipher initCipher(long startBlockNum) {
|
|
|
- final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
|
|
- nonceAndCounterBuf.put(nonce);
|
|
|
- nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
|
|
- final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
|
|
- return aesCtrCipher(fileKey, nonceAndCounter, Cipher.ENCRYPT_MODE);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected Mac initMac() {
|
|
|
- return hmacSha256(hMacMasterKey);
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf) {
|
|
|
- mac.update(iv);
|
|
|
- mac.update(longToByteArray(blockNum));
|
|
|
- mac.update(ciphertextBuf);
|
|
|
- return mac.doFinal();
|
|
|
- }
|
|
|
-
|
|
|
- @Override
|
|
|
- protected void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException {
|
|
|
- try {
|
|
|
- assert ciphertextBuf.remaining() >= cipher.getOutputSize(plaintextBuf.remaining());
|
|
|
- cipher.update(plaintextBuf, ciphertextBuf);
|
|
|
- } catch (ShortBufferException e) {
|
|
|
- throw new EncryptFailedException(e);
|
|
|
- }
|
|
|
- }
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- // read as many blocks from file as possible, but wait if queue is full:
|
|
|
- final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
|
|
|
- final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
|
|
|
- final ReadableByteChannel channel = Channels.newChannel(in);
|
|
|
- int bytesRead = 0;
|
|
|
- long blockNumber = 0;
|
|
|
- final int maxNumBlocks = 64;
|
|
|
- int numBlocks = 0;
|
|
|
- do {
|
|
|
- if (numBlocks < maxNumBlocks) {
|
|
|
- numBlocks++;
|
|
|
- }
|
|
|
- final int inBufSize = numBlocks * CONTENT_MAC_BLOCK;
|
|
|
- final ByteBuffer inBuf = ByteBuffer.allocate(inBufSize);
|
|
|
- bytesRead = readFromChannel(channel, inBuf);
|
|
|
- inBuf.flip();
|
|
|
- final int blocksRead = (int) Math.ceil(bytesRead / (double) CONTENT_MAC_BLOCK);
|
|
|
- final boolean consumedInTime = executor.offer(new BlocksData(inBuf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
|
|
- if (!consumedInTime) {
|
|
|
- break;
|
|
|
- }
|
|
|
- blockNumber += numBlocks;
|
|
|
- } while (bytesRead == numBlocks * CONTENT_MAC_BLOCK);
|
|
|
-
|
|
|
- // wait for encryption workers to finish:
|
|
|
- try {
|
|
|
- executor.waitUntilDone(1, TimeUnit.SECONDS);
|
|
|
- } catch (ExecutionException e) {
|
|
|
- Throwable cause = e;
|
|
|
- while (cause instanceof ExecutionException) {
|
|
|
- cause = cause.getCause();
|
|
|
- }
|
|
|
- if (cause instanceof IOException) {
|
|
|
- throw (IOException) cause;
|
|
|
- } else if (cause instanceof TimeoutException) {
|
|
|
- throw new EncryptFailedException(cause);
|
|
|
- } else if (cause instanceof RuntimeException) {
|
|
|
- throw (RuntimeException) cause;
|
|
|
- } else {
|
|
|
- LOG.error("Unexpected exception", e);
|
|
|
- throw new RuntimeException(cause);
|
|
|
- }
|
|
|
- } finally {
|
|
|
- destroyQuietly(fileKey);
|
|
|
- }
|
|
|
-
|
|
|
- // create and write header:
|
|
|
- final long plaintextSize = in.getRealInputLength();
|
|
|
- final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.allocate(Long.BYTES + fileKeyBytes.length);
|
|
|
- sensitiveHeaderContentBuf.putLong(plaintextSize);
|
|
|
- sensitiveHeaderContentBuf.put(fileKeyBytes);
|
|
|
- headerBuf.clear();
|
|
|
- headerBuf.put(iv);
|
|
|
- headerBuf.put(nonce);
|
|
|
- headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
|
|
|
- headerBuf.flip();
|
|
|
- final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
|
|
- headerMac.update(headerBuf);
|
|
|
- headerBuf.limit(104);
|
|
|
- headerBuf.put(headerMac.doFinal());
|
|
|
- headerBuf.flip();
|
|
|
- encryptedFile.position(0);
|
|
|
- encryptedFile.write(headerBuf);
|
|
|
-
|
|
|
- return plaintextSize;
|
|
|
- }
|
|
|
-
|
|
|
- private byte[] longToByteArray(long lng) {
|
|
|
- return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Reads bytes from a ReadableByteChannel.
|
|
|
- * <p>
|
|
|
- * This implementation guarantees that it will read as many bytes
|
|
|
- * as possible before giving up; this may not always be the case for
|
|
|
- * subclasses of {@link ReadableByteChannel}.
|
|
|
- *
|
|
|
- * @param input the byte channel to read
|
|
|
- * @param buffer byte buffer destination
|
|
|
- * @return the actual length read; may be less than requested if EOF was reached
|
|
|
- * @throws IOException if a read error occurs
|
|
|
- * @see
|
|
|
- * <a href="http://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/IOUtils.html">Apache Commons IOUtils 2.5</a>
|
|
|
- */
|
|
|
- public static int readFromChannel(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
|
|
|
- final int length = buffer.remaining();
|
|
|
- while (buffer.remaining() > 0) {
|
|
|
- final int count = input.read(buffer);
|
|
|
- if (count == -1) { // EOF
|
|
|
- break;
|
|
|
- }
|
|
|
- }
|
|
|
- return length - buffer.remaining();
|
|
|
- }
|
|
|
-
|
|
|
-}
|