mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-23 13:11:28 +00:00
Externalized FileHeader encryption/decryption to separate class
This commit is contained in:
@@ -12,9 +12,6 @@ import org.cryptomator.crypto.engine.FileContentEncryptor;
|
||||
|
||||
class FileContentCryptorImpl implements FileContentCryptor {
|
||||
|
||||
// 16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac
|
||||
static final int HEADER_SIZE = 104;
|
||||
|
||||
private final SecretKey encryptionKey;
|
||||
private final SecretKey macKey;
|
||||
private final SecureRandom randomSource;
|
||||
@@ -27,7 +24,7 @@ class FileContentCryptorImpl implements FileContentCryptor {
|
||||
|
||||
@Override
|
||||
public int getHeaderSize() {
|
||||
return HEADER_SIZE;
|
||||
return FileHeader.HEADER_SIZE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,19 +4,13 @@ import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
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 org.cryptomator.crypto.engine.ByteRange;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
@@ -25,9 +19,7 @@ import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
|
||||
private static final String AES = "AES";
|
||||
private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
|
||||
private static final String AES_CBC = "AES/CBC/PKCS5Padding";
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int CHUNK_SIZE = 32 * 1024;
|
||||
private static final int MAC_SIZE = 32;
|
||||
@@ -36,71 +28,19 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
|
||||
private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD);
|
||||
private final ThreadLocal<Mac> hmacSha256;
|
||||
private final SecretKey contentKey;
|
||||
private final byte[] nonce;
|
||||
private final long cleartextLength;
|
||||
private final FileHeader header;
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header) {
|
||||
this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
|
||||
|
||||
checkHeaderMac(header, hmacSha256.get());
|
||||
|
||||
this.nonce = new byte[8];
|
||||
ByteBuffer nonceBuffer = header.asReadOnlyBuffer();
|
||||
nonceBuffer.position(16).limit(24);
|
||||
nonceBuffer.get(this.nonce);
|
||||
|
||||
byte[] contentKeyBytes = new byte[32];
|
||||
ByteBuffer sensitiveDataBuffer = getCleartextSensitiveHeaderData(header, headerKey);
|
||||
this.cleartextLength = sensitiveDataBuffer.getLong();
|
||||
sensitiveDataBuffer.get(contentKeyBytes);
|
||||
this.contentKey = new SecretKeySpec(contentKeyBytes, AES);
|
||||
|
||||
}
|
||||
|
||||
private static void checkHeaderMac(ByteBuffer header, Mac mac) throws IllegalArgumentException {
|
||||
assert mac.getMacLength() == MAC_SIZE;
|
||||
ByteBuffer headerData = header.asReadOnlyBuffer();
|
||||
headerData.position(0).limit(72);
|
||||
mac.update(headerData);
|
||||
ByteBuffer headerMac = header.asReadOnlyBuffer();
|
||||
headerMac.position(72).limit(72 + MAC_SIZE);
|
||||
byte[] expectedMac = new byte[MAC_SIZE];
|
||||
headerMac.get(expectedMac);
|
||||
|
||||
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
|
||||
throw new IllegalArgumentException("Corrupt header.");
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer getCleartextSensitiveHeaderData(ByteBuffer header, SecretKey headerKey) {
|
||||
try {
|
||||
byte[] iv = new byte[16];
|
||||
ByteBuffer ivBuffer = header.asReadOnlyBuffer();
|
||||
ivBuffer.position(0).limit(16);
|
||||
ivBuffer.get(iv);
|
||||
|
||||
ByteBuffer sensitiveHeaderDataBuffer = header.asReadOnlyBuffer();
|
||||
sensitiveHeaderDataBuffer.position(24).limit(72);
|
||||
|
||||
final Cipher cipher = Cipher.getInstance(AES_CBC);
|
||||
cipher.init(Cipher.DECRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int cleartextLength = cipher.getOutputSize(sensitiveHeaderDataBuffer.remaining());
|
||||
assert cleartextLength == 48 : "decryption shouldn't need more output than input buffer size.";
|
||||
final ByteBuffer cleartext = ByteBuffer.allocate(cleartextLength);
|
||||
cipher.doFinal(sensitiveHeaderDataBuffer, cleartext);
|
||||
cleartext.flip();
|
||||
return cleartext;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to decrypt header.", e);
|
||||
}
|
||||
final ThreadLocalMac hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
|
||||
this.hmacSha256 = hmacSha256;
|
||||
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return cleartextLength;
|
||||
return header.getPayload().getFilesize();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,11 +90,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
contentKey.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// ignore
|
||||
}
|
||||
header.destroy();
|
||||
}
|
||||
|
||||
private class DecryptionJob implements Callable<ByteBuffer> {
|
||||
@@ -175,7 +111,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
macBuf.get(expectedMac);
|
||||
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.put(header.getNonce());
|
||||
nonceAndCounterBuf.putLong(chunkNumber * CHUNK_SIZE / AES_BLOCK_LENGTH_IN_BYTES);
|
||||
this.nonceAndCtr = nonceAndCounterBuf.array();
|
||||
}
|
||||
@@ -191,7 +127,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
}
|
||||
|
||||
Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
cipher.init(Cipher.DECRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr));
|
||||
cipher.init(Cipher.DECRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonceAndCtr));
|
||||
ByteBuffer cleartextChunk = ByteBuffer.allocate(cipher.getOutputSize(ciphertextChunk.remaining()));
|
||||
cipher.update(ciphertextChunk, cleartextChunk);
|
||||
cleartextChunk.flip();
|
||||
|
||||
@@ -3,21 +3,15 @@ package org.cryptomator.crypto.engine.impl;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
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 org.cryptomator.crypto.engine.ByteRange;
|
||||
import org.cryptomator.crypto.engine.FileContentCryptor;
|
||||
@@ -26,20 +20,16 @@ import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final String AES = "AES";
|
||||
private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
|
||||
private static final String AES_CBC = "AES/CBC/PKCS5Padding";
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int CHUNK_SIZE = 32 * 1024;
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
|
||||
private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD);
|
||||
private final ThreadLocal<Mac> hmacSha256;
|
||||
private final ThreadLocalMac hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final SecretKey headerKey;
|
||||
private final SecretKey contentKey;
|
||||
private final byte[] iv;
|
||||
private final byte[] nonce;
|
||||
private final LongAdder cleartextBytesEncrypted = new LongAdder();
|
||||
private ByteBuffer cleartextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
@@ -47,57 +37,13 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
public FileContentEncryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom randomSource) {
|
||||
this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
|
||||
this.headerKey = headerKey;
|
||||
this.iv = new byte[16];
|
||||
this.nonce = new byte[8];
|
||||
final byte[] contentKeyBytes = new byte[32];
|
||||
randomSource.nextBytes(iv);
|
||||
randomSource.nextBytes(nonce);
|
||||
randomSource.nextBytes(contentKeyBytes);
|
||||
this.contentKey = new SecretKeySpec(contentKeyBytes, AES);
|
||||
}
|
||||
|
||||
private ByteBuffer getCleartextSensitiveHeaderData() {
|
||||
ByteBuffer header = ByteBuffer.allocate(104);
|
||||
header.putLong(cleartextBytesEncrypted.sum());
|
||||
header.put(contentKey.getEncoded());
|
||||
header.flip();
|
||||
return header;
|
||||
}
|
||||
|
||||
private ByteBuffer getCiphertextSensitiveHeaderData() {
|
||||
final ByteBuffer cleartext = getCleartextSensitiveHeaderData();
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(AES_CBC);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int ciphertextLength = cipher.getOutputSize(cleartext.remaining());
|
||||
assert ciphertextLength == 48 : "8 byte long and 32 byte file key should fit into 3 blocks";
|
||||
final ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
|
||||
cipher.doFinal(cleartext, ciphertext);
|
||||
ciphertext.flip();
|
||||
return ciphertext;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to compute encrypted header.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] mac(ByteBuffer what) {
|
||||
Mac mac = hmacSha256.get();
|
||||
mac.update(what);
|
||||
return mac.doFinal();
|
||||
this.header = new FileHeader(randomSource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
final ByteBuffer header = ByteBuffer.allocate(FileContentCryptorImpl.HEADER_SIZE);
|
||||
header.put(iv);
|
||||
header.put(nonce);
|
||||
final ByteBuffer sensitiveHeaderData = getCiphertextSensitiveHeaderData();
|
||||
header.put(sensitiveHeaderData);
|
||||
final ByteBuffer headerSoFar = header.asReadOnlyBuffer();
|
||||
headerSoFar.flip();
|
||||
header.put(mac(headerSoFar));
|
||||
header.flip();
|
||||
return header;
|
||||
header.getPayload().setFilesize(cleartextBytesEncrypted.sum());
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,11 +94,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
contentKey.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// ignore
|
||||
}
|
||||
header.destroy();
|
||||
}
|
||||
|
||||
private class EncryptionJob implements Callable<ByteBuffer> {
|
||||
@@ -164,7 +106,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
this.cleartextChunk = cleartextChunk;
|
||||
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.put(header.getNonce());
|
||||
nonceAndCounterBuf.putLong(chunkNumber * CHUNK_SIZE / AES_BLOCK_LENGTH_IN_BYTES);
|
||||
this.nonceAndCtr = nonceAndCounterBuf.array();
|
||||
}
|
||||
@@ -173,7 +115,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
public ByteBuffer call() {
|
||||
try {
|
||||
Cipher cipher = ThreadLocalAesCtrCipher.get();
|
||||
cipher.init(Cipher.ENCRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr));
|
||||
cipher.init(Cipher.ENCRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonceAndCtr));
|
||||
Mac mac = hmacSha256.get();
|
||||
ByteBuffer ciphertextChunk = ByteBuffer.allocate(cipher.getOutputSize(cleartextChunk.remaining()) + mac.getMacLength());
|
||||
cipher.update(cleartextChunk, ciphertextChunk);
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
class FileHeader implements Destroyable {
|
||||
|
||||
static final int HEADER_SIZE = 104;
|
||||
|
||||
private static final int IV_POS = 0;
|
||||
private static final int IV_LEN = 16;
|
||||
private static final int NONCE_POS = 16;
|
||||
private static final int NONCE_LEN = 8;
|
||||
private static final int PAYLOAD_POS = 24;
|
||||
private static final int PAYLOAD_LEN = 48;
|
||||
private static final int MAC_POS = 72;
|
||||
private static final int MAC_LEN = 32;
|
||||
|
||||
private final byte[] iv;
|
||||
private final byte[] nonce;
|
||||
private final FileHeaderPayload payload;
|
||||
|
||||
public FileHeader(SecureRandom randomSource) {
|
||||
this.iv = new byte[IV_LEN];
|
||||
this.nonce = new byte[NONCE_LEN];
|
||||
this.payload = new FileHeaderPayload(randomSource);
|
||||
randomSource.nextBytes(iv);
|
||||
randomSource.nextBytes(nonce);
|
||||
}
|
||||
|
||||
private FileHeader(byte[] iv, byte[] nonce, FileHeaderPayload payload) {
|
||||
this.iv = iv;
|
||||
this.nonce = nonce;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
|
||||
public byte[] getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public FileHeaderPayload getPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
public ByteBuffer toByteBuffer(SecretKey headerKey, Supplier<Mac> hmacSha256Factory) {
|
||||
ByteBuffer result = ByteBuffer.allocate(HEADER_SIZE);
|
||||
result.position(IV_POS).limit(IV_POS + IV_LEN);
|
||||
result.put(iv);
|
||||
result.position(NONCE_POS).limit(NONCE_POS + NONCE_LEN);
|
||||
result.put(nonce);
|
||||
result.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN);
|
||||
result.put(payload.toCiphertextByteBuffer(headerKey, iv));
|
||||
ByteBuffer resultSoFar = result.asReadOnlyBuffer();
|
||||
resultSoFar.flip();
|
||||
Mac mac = hmacSha256Factory.get();
|
||||
assert mac.getMacLength() == MAC_LEN;
|
||||
mac.update(resultSoFar);
|
||||
result.position(MAC_POS).limit(MAC_POS + MAC_LEN);
|
||||
result.put(mac.doFinal());
|
||||
result.flip();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return payload.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
payload.destroy();
|
||||
}
|
||||
|
||||
public static FileHeader decrypt(SecretKey headerKey, Supplier<Mac> hmacSha256Factory, ByteBuffer header) throws IllegalArgumentException {
|
||||
if (header.remaining() != HEADER_SIZE) {
|
||||
throw new IllegalArgumentException("Invalid header size.");
|
||||
}
|
||||
|
||||
checkHeaderMac(header, hmacSha256Factory.get());
|
||||
|
||||
final byte[] iv = new byte[IV_LEN];
|
||||
final ByteBuffer ivBuf = header.asReadOnlyBuffer();
|
||||
ivBuf.position(IV_POS).limit(IV_POS + IV_LEN);
|
||||
ivBuf.get(iv);
|
||||
|
||||
final byte[] nonce = new byte[NONCE_LEN];
|
||||
final ByteBuffer nonceBuf = header.asReadOnlyBuffer();
|
||||
nonceBuf.position(NONCE_POS).limit(NONCE_POS + NONCE_LEN);
|
||||
nonceBuf.get(nonce);
|
||||
|
||||
final ByteBuffer payloadBuf = header.asReadOnlyBuffer();
|
||||
payloadBuf.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN);
|
||||
|
||||
final FileHeaderPayload payload = FileHeaderPayload.fromCiphertextByteBuffer(payloadBuf, headerKey, iv);
|
||||
|
||||
return new FileHeader(iv, nonce, payload);
|
||||
}
|
||||
|
||||
private static void checkHeaderMac(ByteBuffer header, Mac mac) throws IllegalArgumentException {
|
||||
assert mac.getMacLength() == MAC_LEN;
|
||||
ByteBuffer headerData = header.asReadOnlyBuffer();
|
||||
headerData.position(0).limit(MAC_POS);
|
||||
mac.update(headerData);
|
||||
ByteBuffer headerMac = header.asReadOnlyBuffer();
|
||||
headerMac.position(MAC_POS).limit(MAC_POS + MAC_LEN);
|
||||
byte[] expectedMac = new byte[MAC_LEN];
|
||||
headerMac.get(expectedMac);
|
||||
|
||||
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
|
||||
throw new IllegalArgumentException("Corrupt header.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
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;
|
||||
|
||||
class FileHeaderPayload implements Destroyable {
|
||||
|
||||
private static final int FILESIZE_POS = 0;
|
||||
private static final int FILESIZE_LEN = Long.BYTES;
|
||||
private static final int CONTENT_KEY_POS = 8;
|
||||
private static final int CONTENT_KEY_LEN = 32;
|
||||
private static final String AES = "AES";
|
||||
private static final String AES_CBC = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private long filesize;
|
||||
private final SecretKey contentKey;
|
||||
|
||||
public FileHeaderPayload(SecureRandom randomSource) {
|
||||
filesize = 0;
|
||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
||||
try {
|
||||
randomSource.nextBytes(contentKey);
|
||||
this.contentKey = new SecretKeySpec(contentKey, AES);
|
||||
} finally {
|
||||
Arrays.fill(contentKey, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private FileHeaderPayload(long filesize, SecretKey contentKey) {
|
||||
this.filesize = filesize;
|
||||
this.contentKey = contentKey;
|
||||
}
|
||||
|
||||
public long getFilesize() {
|
||||
return filesize;
|
||||
}
|
||||
|
||||
public void setFilesize(long filesize) {
|
||||
this.filesize = filesize;
|
||||
}
|
||||
|
||||
public SecretKey getContentKey() {
|
||||
return contentKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return contentKey.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
try {
|
||||
contentKey.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
private ByteBuffer toCleartextByteBuffer() {
|
||||
ByteBuffer cleartext = ByteBuffer.allocate(FILESIZE_LEN + CONTENT_KEY_LEN);
|
||||
cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN);
|
||||
cleartext.putLong(filesize);
|
||||
cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN);
|
||||
cleartext.put(contentKey.getEncoded());
|
||||
cleartext.flip();
|
||||
return cleartext;
|
||||
}
|
||||
|
||||
public ByteBuffer toCiphertextByteBuffer(SecretKey headerKey, byte[] iv) {
|
||||
final ByteBuffer cleartext = toCleartextByteBuffer();
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(AES_CBC);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int ciphertextLength = cipher.getOutputSize(cleartext.remaining());
|
||||
assert ciphertextLength == 48 : "8 byte long and 32 byte file key should fit into 3 blocks";
|
||||
final ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
|
||||
cipher.doFinal(cleartext, ciphertext);
|
||||
ciphertext.flip();
|
||||
return ciphertext;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to compute encrypted header.", e);
|
||||
} finally {
|
||||
Arrays.fill(cleartext.array(), (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
public static FileHeaderPayload fromCiphertextByteBuffer(ByteBuffer ciphertextPayload, SecretKey headerKey, byte[] iv) {
|
||||
final ByteBuffer cleartext = decryptPayload(ciphertextPayload, headerKey, iv);
|
||||
try {
|
||||
return fromCleartextByteBuffer(cleartext);
|
||||
} finally {
|
||||
// destroy evidence:
|
||||
Arrays.fill(cleartext.array(), (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private static FileHeaderPayload fromCleartextByteBuffer(ByteBuffer cleartext) {
|
||||
final byte[] contentKey = new byte[CONTENT_KEY_LEN];
|
||||
try {
|
||||
cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN);
|
||||
final long filesize = cleartext.getLong();
|
||||
cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN);
|
||||
cleartext.get(contentKey);
|
||||
return new FileHeaderPayload(filesize, new SecretKeySpec(contentKey, AES));
|
||||
} finally {
|
||||
// destroy evidence:
|
||||
Arrays.fill(contentKey, (byte) 0x00);
|
||||
}
|
||||
}
|
||||
|
||||
private static ByteBuffer decryptPayload(ByteBuffer ciphertext, SecretKey headerKey, byte[] iv) {
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(AES_CBC);
|
||||
cipher.init(Cipher.DECRYPT_MODE, headerKey, new IvParameterSpec(iv));
|
||||
final int cleartextLength = cipher.getOutputSize(ciphertext.remaining());
|
||||
assert cleartextLength == ciphertext.remaining() : "decryption shouldn't need more output than input buffer size.";
|
||||
final ByteBuffer cleartext = ByteBuffer.allocate(cleartextLength);
|
||||
cipher.doFinal(ciphertext, cleartext);
|
||||
cleartext.flip();
|
||||
return cleartext;
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Unable to decrypt header.", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
class ThreadLocalMac extends ThreadLocal<Mac> {
|
||||
class ThreadLocalMac extends ThreadLocal<Mac>implements Supplier<Mac> {
|
||||
|
||||
private final SecretKey macKey;
|
||||
private final String macAlgorithm;
|
||||
|
||||
@@ -32,7 +32,7 @@ public class FileContentCryptorTest {
|
||||
public void testShortHeaderInDecryptor() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
|
||||
|
||||
ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
|
||||
@@ -43,7 +43,7 @@ public class FileContentCryptorTest {
|
||||
public void testShortHeaderInEncryptor() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
|
||||
|
||||
ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
|
||||
@@ -54,7 +54,7 @@ public class FileContentCryptorTest {
|
||||
public void testEncryptionAndDecryption() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
||||
|
||||
@@ -19,7 +19,7 @@ public class FileContentDecryptorImplTest {
|
||||
public void testDecryption() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
|
||||
final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHQ==");
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class FileHeaderPayloadTest {
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testEncryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final FileHeaderPayload header = new FileHeaderPayload(RANDOM_MOCK);
|
||||
header.setFilesize(42);
|
||||
final ByteBuffer encrypted = header.toCiphertextByteBuffer(headerKey, new byte[16]);
|
||||
|
||||
// echo -n "AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode | openssl enc -aes-256-cbc -K 0000000000000000000000000000000000000000000000000000000000000000 -iv
|
||||
// 00000000000000000000000000000000 | base64
|
||||
Assert.assertArrayEquals(Base64.decode("S+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5"), Arrays.copyOfRange(encrypted.array(), 0, encrypted.remaining()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final ByteBuffer ciphertextBuf = ByteBuffer.wrap(Base64.decode("S+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5"));
|
||||
final FileHeaderPayload header = FileHeaderPayload.fromCiphertextByteBuffer(ciphertextBuf, headerKey, new byte[16]);
|
||||
Assert.assertEquals(42, header.getFilesize());
|
||||
Assert.assertArrayEquals(new byte[32], header.getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
public class FileHeaderTest {
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@Test
|
||||
public void testEncryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final FileHeader header = new FileHeader(RANDOM_MOCK);
|
||||
header.getPayload().setFilesize(42);
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[8], header.getNonce());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
final ByteBuffer headerAsByteBuffer = header.toByteBuffer(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"));
|
||||
|
||||
// 24 bytes 0x00
|
||||
// + 48 bytes encrypted payload (see FileHeaderPayloadTest)
|
||||
// + 32 bytes HMAC of both (openssl dgst -sha256 -mac HMAC -macopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 -binary)
|
||||
final String expected = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ=";
|
||||
Assert.assertArrayEquals(Base64.decode(expected), Arrays.copyOf(headerAsByteBuffer.array(), headerAsByteBuffer.remaining()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDecryption() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ="));
|
||||
final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
|
||||
Assert.assertEquals(42, header.getPayload().getFilesize());
|
||||
Assert.assertArrayEquals(new byte[16], header.getIv());
|
||||
Assert.assertArrayEquals(new byte[8], header.getNonce());
|
||||
Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecryptionWithInvalidMac1() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iq="));
|
||||
FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void testDecryptionWithInvalidMac2() {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("aAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ="));
|
||||
FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,13 +26,9 @@ import org.cryptomator.filesystem.WritableFile;
|
||||
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CryptoFileSystemTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemTest.class);
|
||||
|
||||
@Test(timeout = 1000)
|
||||
public void testVaultStructureInitialization() throws UncheckedIOException, IOException {
|
||||
// mock cryptor:
|
||||
@@ -56,8 +52,6 @@ public class CryptoFileSystemTest {
|
||||
Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
|
||||
Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
|
||||
Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
|
||||
|
||||
LOG.debug(DirectoryPrinter.print(physicalFs));
|
||||
}
|
||||
|
||||
@Test(timeout = 1000)
|
||||
@@ -104,8 +98,6 @@ public class CryptoFileSystemTest {
|
||||
Assert.assertTrue(fooFolder.exists());
|
||||
Assert.assertTrue(fooBarFolder.exists());
|
||||
Assert.assertEquals(3, countDataFolders(physicalDataRoot)); // parent + foo + bar
|
||||
|
||||
LOG.debug(DirectoryPrinter.print(fs));
|
||||
}
|
||||
|
||||
@Test(timeout = 1000)
|
||||
|
||||
@@ -16,12 +16,10 @@ import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
|
||||
import org.cryptomator.shortening.ShorteningFileSystem;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptAndShortenIntegrationTest {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
|
||||
// private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfLongFolderNames() {
|
||||
@@ -37,17 +35,17 @@ public class EncryptAndShortenIntegrationTest {
|
||||
longFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
|
||||
|
||||
// the long name will produce a metadata file on the physical layer:
|
||||
LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs));
|
||||
// LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs));
|
||||
Assert.assertEquals(1, physicalFs.folder("m").folders().count());
|
||||
|
||||
// on the second layer all .lng files are resolved to their actual names:
|
||||
LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs));
|
||||
// LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs));
|
||||
DirectoryWalker.walk(shorteningFs, node -> {
|
||||
Assert.assertFalse(node.name().endsWith(".lng"));
|
||||
});
|
||||
|
||||
// on the third (cleartext layer) we have cleartext names on the root level:
|
||||
LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs));
|
||||
// LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs));
|
||||
Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user