mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 10:41:26 +00:00
unlock version 5 vaults
This commit is contained in:
@@ -19,11 +19,6 @@ import javax.security.auth.Destroyable;
|
||||
*/
|
||||
public interface FileContentDecryptor extends Destroyable, Closeable {
|
||||
|
||||
/**
|
||||
* @return Number of bytes of the decrypted file.
|
||||
*/
|
||||
long contentLength();
|
||||
|
||||
/**
|
||||
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
|
||||
*
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class Constants {
|
||||
|
||||
private Constants() {
|
||||
}
|
||||
|
||||
static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
|
||||
static final Integer CURRENT_VAULT_VERSION = 4;
|
||||
static final Integer CURRENT_VAULT_VERSION = 5;
|
||||
|
||||
public static final int PAYLOAD_SIZE = 32 * 1024;
|
||||
public static final int NONCE_SIZE = 16;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -110,7 +109,7 @@ class CryptorImpl implements Cryptor {
|
||||
assert keyFile != null;
|
||||
|
||||
// check version
|
||||
if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
|
||||
if (CURRENT_VAULT_VERSION != keyFile.getVersion()) {
|
||||
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
@@ -47,7 +46,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
private final Supplier<Mac> hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final boolean authenticate;
|
||||
private final LongAdder cleartextBytesDecrypted = new LongAdder();
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
@@ -58,11 +56,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return header.getPayload().getFilesize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(ByteBuffer ciphertext) throws InterruptedException {
|
||||
if (ciphertext == FileContentCryptor.EOF) {
|
||||
@@ -105,15 +98,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
@Override
|
||||
public ByteBuffer cleartext() throws InterruptedException {
|
||||
try {
|
||||
final ByteBuffer cleartext = dataProcessor.processedData();
|
||||
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
|
||||
if (bytesUntilLogicalEof <= 0) {
|
||||
return FileContentCryptor.EOF;
|
||||
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
|
||||
cleartext.limit((int) bytesUntilLogicalEof);
|
||||
}
|
||||
cleartextBytesDecrypted.add(cleartext.remaining());
|
||||
return cleartext;
|
||||
return dataProcessor.processedData();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||
throw new AuthenticationFailedException(e);
|
||||
|
||||
@@ -36,8 +36,6 @@ import org.cryptomator.io.ByteBuffers;
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
|
||||
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
@@ -63,7 +61,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
|
||||
header.getPayload().setFilesize(-1l);
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@@ -76,7 +74,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||
if (cleartext == FileContentCryptor.EOF) {
|
||||
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
|
||||
submitCleartextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
@@ -84,19 +81,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
|
||||
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
|
||||
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
int remainingPadding = randomPaddingLength;
|
||||
while (remainingPadding > 0) {
|
||||
int bytesInCurrentIteration = Math.min(remainingPadding, PAYLOAD_SIZE);
|
||||
buf.clear().limit(bytesInCurrentIteration);
|
||||
appendAllAndSubmitIfFull(buf);
|
||||
remainingPadding -= bytesInCurrentIteration;
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.filesystem.crypto;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.CHUNK_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.Constants.PAYLOAD_SIZE;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -72,8 +75,16 @@ class CryptoReadableFile implements ReadableFile {
|
||||
|
||||
@Override
|
||||
public long size() throws UncheckedIOException {
|
||||
assert decryptor != null : "decryptor is always being set during position(long)";
|
||||
return decryptor.contentLength();
|
||||
long ciphertextSize = file.size() - cryptor.getHeaderSize();
|
||||
long overheadPerChunk = CHUNK_SIZE - PAYLOAD_SIZE;
|
||||
long numFullChunks = ciphertextSize / CHUNK_SIZE; // floor by int-truncation
|
||||
long additionalCiphertextBytes = ciphertextSize % CHUNK_SIZE;
|
||||
if (additionalCiphertextBytes > 0 && additionalCiphertextBytes <= overheadPerChunk) {
|
||||
throw new IllegalArgumentException("Method not defined for input value " + ciphertextSize);
|
||||
}
|
||||
long additionalCleartextBytes = (additionalCiphertextBytes == 0) ? 0 : additionalCiphertextBytes - overheadPerChunk;
|
||||
assert additionalCleartextBytes >= 0;
|
||||
return PAYLOAD_SIZE * numFullChunks + additionalCleartextBytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,16 +44,9 @@ class NoFileContentCryptor implements FileContentCryptor {
|
||||
private class Decryptor implements FileContentDecryptor {
|
||||
|
||||
private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
|
||||
private final long contentLength;
|
||||
|
||||
private Decryptor(ByteBuffer header) {
|
||||
assert header.remaining() == Long.BYTES;
|
||||
this.contentLength = header.getLong();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -21,7 +21,7 @@ public class CryptorImplTest {
|
||||
|
||||
@Test
|
||||
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
@@ -31,7 +31,7 @@ public class CryptorImplTest {
|
||||
|
||||
@Test(expected = InvalidPassphraseException.class)
|
||||
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
|
||||
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
@@ -52,7 +52,7 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithMissingVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
@@ -62,7 +62,7 @@ public class CryptorImplTest {
|
||||
@Ignore
|
||||
@Test(expected = UnsupportedVaultFormatException.class)
|
||||
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
|
||||
final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
final String testMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
|
||||
+ "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
|
||||
@@ -72,14 +72,13 @@ public class CryptorImplTest {
|
||||
|
||||
@Test
|
||||
public void testMasterkeyEncryption() throws IOException {
|
||||
final String expectedMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
final String expectedMasterKey = "{\"version\":5,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
|
||||
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
|
||||
+ "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
|
||||
+ "\"versionMac\":\"yuwoRE9GSdgQ2b//qRpTCj3W0qsVLxYVa7/KB3PkfA4=\"}";
|
||||
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
|
||||
cryptor.randomizeMasterkey();
|
||||
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
|
||||
System.out.println(new String(masterkeyFile));
|
||||
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,45 +137,6 @@ public class FileContentCryptorImplTest {
|
||||
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
||||
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
|
||||
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
|
||||
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, ciphertext);
|
||||
}
|
||||
ByteBuffers.copy(encryptor.getHeader(), header);
|
||||
}
|
||||
header.flip();
|
||||
ciphertext.flip();
|
||||
|
||||
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
|
||||
|
||||
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
|
||||
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
|
||||
decryptor.append(ciphertext);
|
||||
decryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, plaintext);
|
||||
}
|
||||
}
|
||||
plaintext.flip();
|
||||
|
||||
byte[] result = new byte[plaintext.remaining()];
|
||||
plaintext.get(result);
|
||||
Assert.assertArrayEquals("hello world".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
|
||||
@@ -95,24 +95,4 @@ public class FileContentEncryptorImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeObfuscation() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
|
||||
|
||||
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK_2, 0)) {
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, result);
|
||||
}
|
||||
result.flip();
|
||||
|
||||
Assert.assertEquals(90, result.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user