Skip to content

Commit

Permalink
simplified key usage in v2 cryptor
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Mar 16, 2021
1 parent 178f06d commit 67720f7
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 81 deletions.
4 changes: 2 additions & 2 deletions src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ class CryptorImpl implements Cryptor {
*/
CryptorImpl(Masterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), random);
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
this.fileContentCryptor = new FileContentCryptorImpl(random);
this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey());
this.fileNameCryptor = new FileNameCryptorImpl(masterkey);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.FileHeaderCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;

Expand All @@ -28,11 +29,11 @@

class FileHeaderCryptorImpl implements FileHeaderCryptor {

private final DestroyableSecretKey headerKey;
private final Masterkey masterkey;
private final SecureRandom random;

FileHeaderCryptorImpl(DestroyableSecretKey headerKey, SecureRandom random) {
this.headerKey = headerKey;
FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.random = random;
}

Expand All @@ -57,12 +58,12 @@ public ByteBuffer encryptHeader(FileHeader header) {
payloadCleartextBuf.putLong(-1l);
payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes());
payloadCleartextBuf.flip();
try (DestroyableSecretKey hk = headerKey.clone()) {
try (DestroyableSecretKey ek = masterkey.getEncKey()) {
ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE);
result.put(headerImpl.getNonce());

// encrypt payload:
Cipher cipher = CipherSupplier.AES_GCM.forEncryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()));
Cipher cipher = CipherSupplier.AES_GCM.forEncryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()));
int encrypted = cipher.doFinal(payloadCleartextBuf, result);
assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN;

Expand Down Expand Up @@ -91,9 +92,9 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic
buf.get(ciphertextAndTag);

ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE);
try (DestroyableSecretKey hk = headerKey.clone()) {
try (DestroyableSecretKey ek = masterkey.getEncKey()) {
// decrypt payload:
Cipher cipher = CipherSupplier.AES_GCM.forDecryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
Cipher cipher = CipherSupplier.AES_GCM.forDecryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf);
assert decrypted == FileHeaderImpl.Payload.SIZE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.siv.SivMode;
Expand All @@ -30,17 +31,15 @@ protected SivMode initialValue() {
};
};

private final DestroyableSecretKey encryptionKey;
private final DestroyableSecretKey macKey;
private final Masterkey masterkey;

FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) {
this.encryptionKey = encryptionKey;
this.macKey = macKey;
FileNameCryptorImpl(Masterkey masterkey) {
this.masterkey = masterkey;
}

@Override
public String hashDirectoryId(String cleartextDirectoryId) {
try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes);
byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes);
Expand All @@ -55,7 +54,7 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) {

@Override
public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData);
return encoding.encode(encryptedBytes);
Expand All @@ -69,11 +68,11 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t

@Override
public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) {
byte[] encryptedBytes = encoding.decode(ciphertextName);
byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (UnauthenticCiphertextException | IllegalBlockSizeException e) {
} catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) {
throw new AuthenticationFailedException("Invalid Ciphertext.", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
Expand Down Expand Up @@ -56,9 +57,9 @@ public class FileContentCryptorImplTest {

@BeforeEach
public void setup() {
DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES");
Masterkey masterkey = new Masterkey(new byte[64]);
header = new FileHeaderImpl(new byte[12], new byte[32]);
headerCryptor = new FileHeaderCryptorImpl(encKey, CSPRNG);
headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG);
fileContentCryptor = new FileContentCryptorImpl(CSPRNG);
cryptor = Mockito.mock(Cryptor.class);
Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.openjdk.jmh.annotations.Benchmark;
Expand Down Expand Up @@ -38,8 +39,8 @@
public class FileHeaderCryptorBenchmark {

private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES");
private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, RANDOM_MOCK);
private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK);

private ByteBuffer validHeaderCiphertextBuf;
private FileHeader header;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
Expand All @@ -35,14 +36,14 @@ public class FileHeaderCryptorImplTest {

@BeforeEach
public void setup() {
DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES");
headerCryptor = new FileHeaderCryptorImpl(encKey, RANDOM_MOCK);
Masterkey masterkey = new Masterkey(new byte[64]);
headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);

// create new (unused) cipher, just to cipher.init() internally. This is an attempt to avoid
// InvalidAlgorithmParameterExceptions due to IV-reuse, when the actual unit tests use constant IVs
byte[] nonce = new byte[GCM_NONCE_SIZE];
ANTI_REUSE_PRNG.nextBytes(nonce);
Cipher cipher = CipherSupplier.AES_GCM.forEncryption(encKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
Cipher cipher = CipherSupplier.AES_GCM.forEncryption(masterkey.getEncKey(), new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce));
Assertions.assertNotNull(cipher);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,109 +8,124 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v2;

import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.siv.UnauthenticCiphertextException;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.stream.Stream;


public class FileNameCryptorImplTest {

private static final Charset UTF_8 = StandardCharsets.UTF_8;

@Test
public void testDeterministicEncryptionOfFilenames() throws AuthenticationFailedException {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

// some random
for (int i = 0; i < 2000; i++) {
final String origName = UUID.randomUUID().toString();
final String encrypted1 = filenameCryptor.encryptFilename(origName);
final String encrypted2 = filenameCryptor.encryptFilename(origName);
Assertions.assertEquals(encrypted1, encrypted2);
final String decrypted = filenameCryptor.decryptFilename(encrypted1);
Assertions.assertEquals(origName, decrypted);
}
private final Masterkey masterkey = new Masterkey(new byte[64]);
private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey);

private static Stream<String> filenameGenerator() {
return Stream.generate(UUID::randomUUID).map(UUID::toString).limit(100);
}

@DisplayName("encrypt and decrypt file names")
@ParameterizedTest(name = "decrypt(encrypt({0}))")
@MethodSource("filenameGenerator")
public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException {
String encrypted1 = filenameCryptor.encryptFilename(origName);
String encrypted2 = filenameCryptor.encryptFilename(origName);
String decrypted = filenameCryptor.decryptFilename(encrypted1);

Assertions.assertEquals(encrypted1, encrypted2);
Assertions.assertEquals(origName, decrypted);
}

@DisplayName("encrypt and decrypt file names with AD and custom encoding")
@ParameterizedTest(name = "decrypt(encrypt({0}))")
@MethodSource("filenameGenerator")
public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociatedData(String origName) throws AuthenticationFailedException {
byte[] associdatedData = new byte[10];
String encrypted1 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData);
String encrypted2 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData);
String decrypted = filenameCryptor.decryptFilename(BaseEncoding.base64Url(), encrypted1, associdatedData);

Assertions.assertEquals(encrypted1, encrypted2);
Assertions.assertEquals(origName, decrypted);
}

@Test
@DisplayName("encrypt and decrypt 128 bit filename")
public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException {
// block size length file names
final String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii
final String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3);
final String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3);
String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii
String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3);
String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3);
String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a);

Assertions.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a);
Assertions.assertEquals(originalPath3, decryptedPath3);
}

@DisplayName("hash directory id for random directory ids")
@ParameterizedTest(name = "hashDirectoryId({0})")
@MethodSource("filenameGenerator")
public void testDeterministicHashingOfDirectoryIds(String originalDirectoryId) {
final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId);
final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId);
Assertions.assertEquals(hashedDirectory1, hashedDirectory2);
}

@Test
public void testDeterministicHashingOfDirectoryIds() throws IOException {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

// some random
for (int i = 0; i < 2000; i++) {
final String originalDirectoryId = UUID.randomUUID().toString();
final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId);
final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId);
Assertions.assertEquals(hashedDirectory1, hashedDirectory2);
}
@DisplayName("decrypt non-ciphertext")
public void testDecryptionOfMalformedFilename() {
AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
filenameCryptor.decryptFilename("lol");
});
MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class));
}

@Test
@DisplayName("decrypt tampered ciphertext")
public void testDecryptionOfManipulatedFilename() {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8);
encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte
Assertions.assertThrows(AuthenticationFailedException.class, () -> {

AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
filenameCryptor.decryptFilename(new String(encrypted, UTF_8));
});
MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class));
}

@Test
@DisplayName("encrypt with different AD")
public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8));
final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8));
Assertions.assertNotEquals(encrypted1, encrypted2);
}

@Test
@DisplayName("decrypt ciphertext with correct AD")
public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8));
final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8));
Assertions.assertEquals("test", decrypted);
}

@Test
@DisplayName("decrypt ciphertext with incorrect AD")
public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() {
final byte[] keyBytes = new byte[32];
final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES");
final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES");
final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey);

final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8));

Assertions.assertThrows(AuthenticationFailedException.class, () -> {
filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8));
});
Expand Down

0 comments on commit 67720f7

Please sign in to comment.