* Furthermore, this implementation will not create copies when accessing {@link #getEncoded()}. - * Instead it implements {@link #copy} and {@link AutoCloseable} in an exception-free manner. To prevent mutation of the exposed key, + * Instead, it implements {@link #copy} and {@link AutoCloseable} in an exception-free manner. To prevent mutation of the exposed key, * you would want to make sure to always work on scoped copies, such as in this example: * *
diff --git a/src/main/java/org/cryptomator/cryptolib/common/HKDFHelper.java b/src/main/java/org/cryptomator/cryptolib/common/HKDFHelper.java
new file mode 100644
index 0000000..bc50883
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/common/HKDFHelper.java
@@ -0,0 +1,32 @@
+package org.cryptomator.cryptolib.common;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.bouncycastle.crypto.DerivationFunction;
+import org.bouncycastle.crypto.Digest;
+import org.bouncycastle.crypto.digests.SHA512Digest;
+import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
+import org.bouncycastle.crypto.params.HKDFParameters;
+
+public class HKDFHelper {
+
+ /**
+ * Derives a key from the given input keying material (IKM) using the HMAC-based Key Derivation Function (HKDF) with the SHA-512 hash function.
+ * @param salt The optional salt (can be an empty byte array)
+ * @param ikm The input keying material
+ * @param info The optional context (can be an empty byte array)
+ * @param length Desired output key length
+ * @return The derived key
+ * @implNote This method uses the Bouncy Castle library for HKDF computation.
+ */
+ public static byte[] hkdfSha512(byte[] salt, byte[] ikm, byte[] info, int length) {
+ return hkdf(new SHA512Digest(), salt, ikm, info, length);
+ }
+
+ @VisibleForTesting static byte[] hkdf(Digest digest, byte[] salt, byte[] ikm, byte[] info, int length) {
+ byte[] result = new byte[length];
+ DerivationFunction hkdf = new HKDFBytesGenerator(digest);
+ hkdf.init(new HKDFParameters(ikm, salt, info));
+ hkdf.generateBytes(result, 0, length);
+ return result;
+ }
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
index dc43d97..984cd83 100644
--- a/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
+++ b/src/main/java/org/cryptomator/cryptolib/common/MasterkeyFileAccess.java
@@ -4,6 +4,7 @@
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import javax.crypto.Mac;
import java.io.ByteArrayInputStream;
@@ -99,7 +100,7 @@ public void changePassphrase(InputStream oldIn, OutputStream newOut, CharSequenc
// visible for testing
MasterkeyFile changePassphrase(MasterkeyFile masterkey, CharSequence oldPassphrase, CharSequence newPassphrase) throws InvalidPassphraseException {
- try (Masterkey key = unlock(masterkey, oldPassphrase)) {
+ try (PerpetualMasterkey key = unlock(masterkey, oldPassphrase)) {
return lock(key, newPassphrase, masterkey.version, masterkey.scryptCostParam);
}
}
@@ -114,7 +115,7 @@ MasterkeyFile changePassphrase(MasterkeyFile masterkey, CharSequence oldPassphra
* @throws InvalidPassphraseException If the provided passphrase can not be used to unwrap the stored keys.
* @throws MasterkeyLoadingFailedException If reading the masterkey file fails
*/
- public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLoadingFailedException {
+ public PerpetualMasterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLoadingFailedException {
try (InputStream in = Files.newInputStream(filePath, StandardOpenOption.READ)) {
return load(in, passphrase);
} catch (IOException e) {
@@ -122,7 +123,7 @@ public Masterkey load(Path filePath, CharSequence passphrase) throws MasterkeyLo
}
}
- public Masterkey load(InputStream in, CharSequence passphrase) throws IOException {
+ public PerpetualMasterkey load(InputStream in, CharSequence passphrase) throws IOException {
try (Reader reader = new InputStreamReader(in, UTF_8)) {
MasterkeyFile parsedFile = MasterkeyFile.read(reader);
if (!parsedFile.isValid()) {
@@ -134,14 +135,14 @@ public Masterkey load(InputStream in, CharSequence passphrase) throws IOExceptio
}
// visible for testing
- Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws InvalidPassphraseException {
+ PerpetualMasterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws InvalidPassphraseException {
Preconditions.checkNotNull(parsedFile);
Preconditions.checkArgument(parsedFile.isValid(), "Invalid masterkey file");
Preconditions.checkNotNull(passphrase);
try (DestroyableSecretKey kek = scrypt(passphrase, parsedFile.scryptSalt, pepper, parsedFile.scryptCostParam, parsedFile.scryptBlockSize);
- DestroyableSecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, Masterkey.ENC_ALG);
- DestroyableSecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, Masterkey.MAC_ALG)) {
+ DestroyableSecretKey encKey = AesKeyWrap.unwrap(kek, parsedFile.encMasterKey, PerpetualMasterkey.ENC_ALG);
+ DestroyableSecretKey macKey = AesKeyWrap.unwrap(kek, parsedFile.macMasterKey, PerpetualMasterkey.MAC_ALG)) {
return Masterkey.from(encKey, macKey);
} catch (InvalidKeyException e) {
throw new InvalidPassphraseException();
@@ -158,11 +159,11 @@ Masterkey unlock(MasterkeyFile parsedFile, CharSequence passphrase) throws Inval
* @param passphrase The passphrase used during key derivation
* @throws IOException When unable to write to the given file
*/
- public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase) throws IOException {
+ public void persist(PerpetualMasterkey masterkey, Path filePath, CharSequence passphrase) throws IOException {
persist(masterkey, filePath, passphrase, DEFAULT_MASTERKEY_FILE_VERSION);
}
- public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException {
+ public void persist(PerpetualMasterkey masterkey, Path filePath, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException {
Path tmpFilePath = filePath.resolveSibling(filePath.getFileName().toString() + ".tmp");
try (OutputStream out = Files.newOutputStream(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
persist(masterkey, out, passphrase, vaultVersion);
@@ -170,12 +171,12 @@ public void persist(Masterkey masterkey, Path filePath, CharSequence passphrase,
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
}
- public void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException {
+ public void persist(PerpetualMasterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion) throws IOException {
persist(masterkey, out, passphrase, vaultVersion, DEFAULT_SCRYPT_COST_PARAM);
}
// visible for testing
- void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion, int scryptCostParam) throws IOException {
+ void persist(PerpetualMasterkey masterkey, OutputStream out, CharSequence passphrase, @Deprecated int vaultVersion, int scryptCostParam) throws IOException {
Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed");
MasterkeyFile fileContent = lock(masterkey, passphrase, vaultVersion, scryptCostParam);
@@ -185,7 +186,7 @@ void persist(Masterkey masterkey, OutputStream out, CharSequence passphrase, @De
}
// visible for testing
- MasterkeyFile lock(Masterkey masterkey, CharSequence passphrase, int vaultVersion, int scryptCostParam) {
+ MasterkeyFile lock(PerpetualMasterkey masterkey, CharSequence passphrase, int vaultVersion, int scryptCostParam) {
Preconditions.checkNotNull(masterkey);
Preconditions.checkNotNull(passphrase);
Preconditions.checkArgument(!masterkey.isDestroyed(), "masterkey has been destroyed");
@@ -212,9 +213,9 @@ private static DestroyableSecretKey scrypt(CharSequence passphrase, byte[] salt,
byte[] saltAndPepper = new byte[salt.length + pepper.length];
System.arraycopy(salt, 0, saltAndPepper, 0, salt.length);
System.arraycopy(pepper, 0, saltAndPepper, salt.length, pepper.length);
- byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, Masterkey.SUBKEY_LEN_BYTES);
+ byte[] kekBytes = Scrypt.scrypt(passphrase, saltAndPepper, costParam, blockSize, PerpetualMasterkey.SUBKEY_LEN_BYTES);
try {
- return new DestroyableSecretKey(kekBytes, Masterkey.ENC_ALG);
+ return new DestroyableSecretKey(kekBytes, PerpetualMasterkey.ENC_ALG);
} finally {
Arrays.fill(kekBytes, (byte) 0x00);
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
index 4662fae..26a4e5e 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/Constants.java
@@ -1,11 +1,3 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
package org.cryptomator.cryptolib.v1;
final class Constants {
@@ -13,6 +5,8 @@ final class Constants {
private Constants() {
}
+ static final String C9R_FILE_EXT = ".c9r";
+
static final String CONTENT_ENC_ALG = "AES";
static final int NONCE_SIZE = 16;
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
index e34137f..fe7eabf 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java
@@ -1,21 +1,17 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
package org.cryptomator.cryptolib.v1;
import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import java.security.SecureRandom;
class CryptorImpl implements Cryptor {
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
private final FileContentCryptorImpl fileContentCryptor;
private final FileHeaderCryptorImpl fileHeaderCryptor;
private final FileNameCryptorImpl fileNameCryptor;
@@ -24,7 +20,7 @@ class CryptorImpl implements Cryptor {
* Package-private constructor.
* Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
*/
- CryptorImpl(Masterkey masterkey, SecureRandom random) {
+ CryptorImpl(PerpetualMasterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
this.fileContentCryptor = new FileContentCryptorImpl(masterkey, random);
@@ -43,12 +39,27 @@ public FileHeaderCryptorImpl fileHeaderCryptor() {
return fileHeaderCryptor;
}
+ @Override
+ public FileHeaderCryptor fileHeaderCryptor(int revision) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public FileNameCryptorImpl fileNameCryptor() {
assertNotDestroyed();
return fileNameCryptor;
}
+ @Override
+ public FileNameCryptor fileNameCryptor(int revision) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DirectoryContentCryptor directoryContentCryptor() {
+ return new DirectoryContentCryptorImpl(this);
+ }
+
@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java
index fad2b5a..7e02917 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorProviderImpl.java
@@ -8,8 +8,10 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v1;
+import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
import java.security.SecureRandom;
@@ -22,8 +24,13 @@ public Scheme scheme() {
}
@Override
- public CryptorImpl provide(Masterkey masterkey, SecureRandom random) {
- return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random));
+ public Cryptor provide(Masterkey masterkey, SecureRandom random) {
+ if (masterkey instanceof PerpetualMasterkey) {
+ PerpetualMasterkey perpetualMasterkey = (PerpetualMasterkey) masterkey;
+ return new CryptorImpl(perpetualMasterkey, ReseedingSecureRandom.create(random));
+ } else {
+ throw new IllegalArgumentException("V1 Cryptor requires a PerpetualMasterkey.");
+ }
}
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImpl.java
new file mode 100644
index 0000000..85d4c57
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImpl.java
@@ -0,0 +1,87 @@
+package org.cryptomator.cryptolib.v1;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+import static org.cryptomator.cryptolib.v1.Constants.C9R_FILE_EXT;
+
+class DirectoryContentCryptorImpl implements DirectoryContentCryptor {
+
+ private final CryptorImpl cryptor;
+
+ public DirectoryContentCryptorImpl(CryptorImpl cryptor) {
+ this.cryptor = cryptor;
+ }
+
+ // DIRECTORY METADATA
+
+ @Override
+ public DirectoryMetadataImpl rootDirectoryMetadata() {
+ return new DirectoryMetadataImpl(new byte[0]);
+ }
+
+ @Override
+ public DirectoryMetadataImpl newDirectoryMetadata() {
+ byte[] dirId = UUID.randomUUID().toString().getBytes(StandardCharsets.US_ASCII);
+ return new DirectoryMetadataImpl(dirId);
+ }
+
+ @Override
+ public DirectoryMetadataImpl decryptDirectoryMetadata(byte[] ciphertext) {
+ // dirId is stored in plaintext
+ return new DirectoryMetadataImpl(ciphertext);
+ }
+
+ @Override
+ public byte[] encryptDirectoryMetadata(DirectoryMetadata directoryMetadata) {
+ // dirId is stored in plaintext
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ return metadataImpl.dirId();
+ }
+
+ // DIR PATH
+
+ @Override
+ public String dirPath(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ String dirIdStr = cryptor.fileNameCryptor().hashDirectoryId(metadataImpl.dirId());
+ assert dirIdStr.length() == 32;
+ return "d/" + dirIdStr.substring(0, 2) + "/" + dirIdStr.substring(2);
+ }
+
+ // FILE NAMES
+
+ @Override
+ public Decrypting fileNameDecryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
+ return ciphertextAndExt -> {
+ String ciphertext = removeExtension(ciphertextAndExt);
+ return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId);
+ };
+ }
+
+ @Override
+ public Encrypting fileNameEncryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
+ return plaintext -> {
+ String ciphertext = fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), plaintext, dirId);
+ return ciphertext + C9R_FILE_EXT;
+ };
+ }
+
+ private static String removeExtension(String filename) {
+ if (filename.endsWith(C9R_FILE_EXT)) {
+ return filename.substring(0, filename.length() - C9R_FILE_EXT.length());
+ } else {
+ throw new IllegalArgumentException("Not a " + C9R_FILE_EXT + " file: " + filename);
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/DirectoryMetadataImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/DirectoryMetadataImpl.java
new file mode 100644
index 0000000..f21762d
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v1/DirectoryMetadataImpl.java
@@ -0,0 +1,25 @@
+package org.cryptomator.cryptolib.v1;
+
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+
+class DirectoryMetadataImpl implements DirectoryMetadata {
+
+ private final byte[] dirId;
+
+ public DirectoryMetadataImpl(byte[] dirId) {
+ this.dirId = dirId;
+ }
+
+ static DirectoryMetadataImpl cast(DirectoryMetadata metadata) {
+ if (metadata instanceof DirectoryMetadataImpl) {
+ return (DirectoryMetadataImpl) metadata;
+ } else {
+ throw new IllegalArgumentException("Unsupported metadata type " + metadata.getClass());
+ }
+ }
+
+ public byte[] dirId() {
+ return dirId;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
index f7cf465..d974f40 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileContentCryptorImpl.java
@@ -1,9 +1,6 @@
package org.cryptomator.cryptolib.v1;
-import org.cryptomator.cryptolib.api.AuthenticationFailedException;
-import org.cryptomator.cryptolib.api.FileContentCryptor;
-import org.cryptomator.cryptolib.api.FileHeader;
-import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.*;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MacSupplier;
@@ -27,10 +24,10 @@
class FileContentCryptorImpl implements FileContentCryptor {
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
private final SecureRandom random;
- FileContentCryptorImpl(Masterkey masterkey, SecureRandom random) {
+ FileContentCryptorImpl(PerpetualMasterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.random = random;
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
index f1b0c59..ed7663a 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImpl.java
@@ -8,10 +8,7 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v1;
-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.api.*;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MacSupplier;
@@ -30,10 +27,10 @@
class FileHeaderCryptorImpl implements FileHeaderCryptor {
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
private final SecureRandom random;
- FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) {
+ FileHeaderCryptorImpl(PerpetualMasterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.random = random;
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
index fbed9fa..31eab10 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileHeaderImpl.java
@@ -81,9 +81,9 @@ public static class Payload implements Destroyable {
private long reserved;
private final DestroyableSecretKey contentKey;
- Payload(long reversed, byte[] contentKeyBytes) {
+ Payload(long reserved, byte[] contentKeyBytes) {
Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
- this.reserved = reversed;
+ this.reserved = reserved;
this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
index 5104aa5..ddf6ba0 100644
--- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java
@@ -12,6 +12,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.cryptolib.common.ObjectPool;
@@ -28,19 +29,18 @@ class FileNameCryptorImpl implements FileNameCryptor {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
- FileNameCryptorImpl(Masterkey masterkey) {
+ FileNameCryptorImpl(PerpetualMasterkey masterkey) {
this.masterkey = masterkey;
}
@Override
- public String hashDirectoryId(String cleartextDirectoryId) {
+ public String hashDirectoryId(byte[] cleartextDirectoryId) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
ObjectPool.Lease siv = AES_SIV.get()) {
- byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
- byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId);
byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
index f6c0f4d..5e4be82 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/Constants.java
@@ -1,11 +1,3 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
package org.cryptomator.cryptolib.v2;
final class Constants {
@@ -13,6 +5,8 @@ final class Constants {
private Constants() {
}
+ static final String C9R_FILE_EXT = ".c9r";
+
static final String CONTENT_ENC_ALG = "AES";
static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
index 7389ccd..ac46a8b 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java
@@ -1,22 +1,18 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
package org.cryptomator.cryptolib.v2;
import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
import java.security.SecureRandom;
class CryptorImpl implements Cryptor {
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
private final FileContentCryptorImpl fileContentCryptor;
private final FileHeaderCryptorImpl fileHeaderCryptor;
private final FileNameCryptorImpl fileNameCryptor;
@@ -25,7 +21,7 @@ class CryptorImpl implements Cryptor {
* Package-private constructor.
* Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
*/
- CryptorImpl(Masterkey masterkey, SecureRandom random) {
+ CryptorImpl(PerpetualMasterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random);
this.fileContentCryptor = new FileContentCryptorImpl(random);
@@ -44,12 +40,27 @@ public FileHeaderCryptorImpl fileHeaderCryptor() {
return fileHeaderCryptor;
}
+ @Override
+ public FileHeaderCryptor fileHeaderCryptor(int revision) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public FileNameCryptorImpl fileNameCryptor() {
assertNotDestroyed();
return fileNameCryptor;
}
+ @Override
+ public FileNameCryptor fileNameCryptor(int revision) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public DirectoryContentCryptor directoryContentCryptor() {
+ return new DirectoryContentCryptorImpl(this);
+ }
+
@Override
public boolean isDestroyed() {
return masterkey.isDestroyed();
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java
index 1a6a018..5fb4113 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorProviderImpl.java
@@ -8,8 +8,10 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v2;
+import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
import java.security.SecureRandom;
@@ -22,8 +24,13 @@ public Scheme scheme() {
}
@Override
- public CryptorImpl provide(Masterkey masterkey, SecureRandom random) {
- return new CryptorImpl(masterkey, ReseedingSecureRandom.create(random));
+ public Cryptor provide(Masterkey masterkey, SecureRandom random) {
+ if (masterkey instanceof PerpetualMasterkey) {
+ PerpetualMasterkey perpetualMasterkey = (PerpetualMasterkey) masterkey;
+ return new CryptorImpl(perpetualMasterkey, ReseedingSecureRandom.create(random));
+ } else {
+ throw new IllegalArgumentException("V2 Cryptor requires a PerpetualMasterkey.");
+ }
}
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImpl.java
new file mode 100644
index 0000000..09938ce
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImpl.java
@@ -0,0 +1,87 @@
+package org.cryptomator.cryptolib.v2;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+import static org.cryptomator.cryptolib.v2.Constants.C9R_FILE_EXT;
+
+class DirectoryContentCryptorImpl implements DirectoryContentCryptor {
+
+ private final CryptorImpl cryptor;
+
+ public DirectoryContentCryptorImpl(CryptorImpl cryptor) {
+ this.cryptor = cryptor;
+ }
+
+ // DIRECTORY METADATA
+
+ @Override
+ public DirectoryMetadataImpl rootDirectoryMetadata() {
+ return new DirectoryMetadataImpl(new byte[0]);
+ }
+
+ @Override
+ public DirectoryMetadataImpl newDirectoryMetadata() {
+ byte[] dirId = UUID.randomUUID().toString().getBytes(StandardCharsets.US_ASCII);
+ return new DirectoryMetadataImpl(dirId);
+ }
+
+ @Override
+ public DirectoryMetadataImpl decryptDirectoryMetadata(byte[] ciphertext) {
+ // dirId is stored in plaintext
+ return new DirectoryMetadataImpl(ciphertext);
+ }
+
+ @Override
+ public byte[] encryptDirectoryMetadata(DirectoryMetadata directoryMetadata) {
+ // dirId is stored in plaintext
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ return metadataImpl.dirId();
+ }
+
+ // DIR PATH
+
+ @Override
+ public String dirPath(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ String dirIdStr = cryptor.fileNameCryptor().hashDirectoryId(metadataImpl.dirId());
+ assert dirIdStr.length() == 32;
+ return "d/" + dirIdStr.substring(0, 2) + "/" + dirIdStr.substring(2);
+ }
+
+ // FILE NAMES
+
+ @Override
+ public Decrypting fileNameDecryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
+ return ciphertextAndExt -> {
+ String ciphertext = removeExtension(ciphertextAndExt);
+ return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId);
+ };
+ }
+
+ @Override
+ public Encrypting fileNameEncryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor();
+ return plaintext -> {
+ String ciphertext = fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), plaintext, dirId);
+ return ciphertext + C9R_FILE_EXT;
+ };
+ }
+
+ private static String removeExtension(String filename) {
+ if (filename.endsWith(C9R_FILE_EXT)) {
+ return filename.substring(0, filename.length() - C9R_FILE_EXT.length());
+ } else {
+ throw new IllegalArgumentException("Not a " + C9R_FILE_EXT + " file: " + filename);
+ }
+ }
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/DirectoryMetadataImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/DirectoryMetadataImpl.java
new file mode 100644
index 0000000..d57756b
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v2/DirectoryMetadataImpl.java
@@ -0,0 +1,25 @@
+package org.cryptomator.cryptolib.v2;
+
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+
+class DirectoryMetadataImpl implements DirectoryMetadata {
+
+ private final byte[] dirId;
+
+ public DirectoryMetadataImpl(byte[] dirId) {
+ this.dirId = dirId;
+ }
+
+ static DirectoryMetadataImpl cast(DirectoryMetadata metadata) {
+ if (metadata instanceof DirectoryMetadataImpl) {
+ return (DirectoryMetadataImpl) metadata;
+ } else {
+ throw new IllegalArgumentException("Unsupported metadata type " + metadata.getClass());
+ }
+ }
+
+ public byte[] dirId() {
+ return dirId;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
index 35bebc1..4cba09c 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java
@@ -8,10 +8,7 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v2;
-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.api.*;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.ObjectPool;
@@ -30,10 +27,10 @@
class FileHeaderCryptorImpl implements FileHeaderCryptor {
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
private final SecureRandom random;
- FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) {
+ FileHeaderCryptorImpl(PerpetualMasterkey masterkey, SecureRandom random) {
this.masterkey = masterkey;
this.random = random;
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
index 39bcbbc..94a266c 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderImpl.java
@@ -74,16 +74,16 @@ public void destroy() {
public static class Payload implements Destroyable {
- static final int REVERSED_LEN = Long.BYTES;
+ static final int RESERVED_LEN = Long.BYTES;
static final int CONTENT_KEY_LEN = 32;
- static final int SIZE = REVERSED_LEN + CONTENT_KEY_LEN;
+ static final int SIZE = RESERVED_LEN + CONTENT_KEY_LEN;
private long reserved;
private final DestroyableSecretKey contentKey;
- Payload(long reversed, byte[] contentKeyBytes) {
+ Payload(long reserved, byte[] contentKeyBytes) {
Preconditions.checkArgument(contentKeyBytes.length == CONTENT_KEY_LEN, "Invalid key length. (was: " + contentKeyBytes.length + ", required: " + CONTENT_KEY_LEN + ")");
- this.reserved = reversed;
+ this.reserved = reserved;
this.contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
index 0498afe..c56d622 100644
--- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
+++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java
@@ -12,6 +12,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileNameCryptor;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
import org.cryptomator.cryptolib.common.ObjectPool;
@@ -28,19 +29,18 @@ class FileNameCryptorImpl implements FileNameCryptor {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
- private final Masterkey masterkey;
+ private final PerpetualMasterkey masterkey;
- FileNameCryptorImpl(Masterkey masterkey) {
+ FileNameCryptorImpl(PerpetualMasterkey masterkey) {
this.masterkey = masterkey;
}
@Override
- public String hashDirectoryId(String cleartextDirectoryId) {
+ public String hashDirectoryId(byte[] cleartextDirectoryId) {
try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey();
ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance();
ObjectPool.Lease siv = AES_SIV.get()) {
- byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
- byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes);
+ byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId);
byte[] hashedBytes = sha1.get().digest(encryptedBytes);
return BASE32.encode(hashedBytes);
}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/Constants.java b/src/main/java/org/cryptomator/cryptolib/v3/Constants.java
new file mode 100644
index 0000000..aec90da
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/Constants.java
@@ -0,0 +1,19 @@
+package org.cryptomator.cryptolib.v3;
+
+final class Constants {
+
+ private Constants() {
+ }
+
+ static final String UVF_FILE_EXT = ".uvf";
+
+ static final String CONTENT_ENC_ALG = "AES";
+
+ static final byte[] UVF_MAGIC_BYTES = new byte[]{'u', 'v', 'f', 0x00}; // TODO increase version number when adopting final spec
+
+ static final int GCM_NONCE_SIZE = 12; // 96 bit IVs strongly recommended for GCM
+ static final int PAYLOAD_SIZE = 32 * 1024;
+ static final int GCM_TAG_SIZE = 16;
+ static final int CHUNK_SIZE = GCM_NONCE_SIZE + PAYLOAD_SIZE + GCM_TAG_SIZE;
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java
new file mode 100644
index 0000000..c0947d7
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java
@@ -0,0 +1,83 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeaderCryptor;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.RevolvingMasterkey;
+import org.cryptomator.cryptolib.v1.CryptorProviderImpl;
+
+import java.security.SecureRandom;
+
+class CryptorImpl implements Cryptor {
+
+ private final RevolvingMasterkey masterkey;
+ private final FileContentCryptorImpl fileContentCryptor;
+ private final SecureRandom random;
+
+ /**
+ * Package-private constructor.
+ * Use {@link CryptorProviderImpl#provide(Masterkey, SecureRandom)} to obtain a Cryptor instance.
+ */
+ CryptorImpl(RevolvingMasterkey masterkey, SecureRandom random) {
+ this.masterkey = masterkey;
+ this.fileContentCryptor = new FileContentCryptorImpl(random);
+ this.random = random;
+ }
+
+ @Override
+ public FileContentCryptorImpl fileContentCryptor() {
+ assertNotDestroyed();
+ return fileContentCryptor;
+ }
+
+ @Override
+ public FileHeaderCryptorImpl fileHeaderCryptor() {
+ return fileHeaderCryptor(masterkey.currentRevision());
+ }
+
+ @Override
+ public FileHeaderCryptorImpl fileHeaderCryptor(int revision) {
+ assertNotDestroyed();
+ return new FileHeaderCryptorImpl(masterkey, random, revision);
+ }
+
+ @Override
+ public FileNameCryptorImpl fileNameCryptor() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public FileNameCryptorImpl fileNameCryptor(int revision) {
+ assertNotDestroyed();
+ return new FileNameCryptorImpl(masterkey, revision);
+ }
+
+ @Override
+ public DirectoryContentCryptorImpl directoryContentCryptor() {
+ return new DirectoryContentCryptorImpl(masterkey, random, this);
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return masterkey.isDestroyed();
+ }
+
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ @Override
+ public void destroy() {
+ masterkey.destroy();
+ }
+
+ private void assertNotDestroyed() {
+ if (isDestroyed()) {
+ throw new IllegalStateException("Cryptor destroyed.");
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/CryptorProviderImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/CryptorProviderImpl.java
new file mode 100644
index 0000000..c9082aa
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/CryptorProviderImpl.java
@@ -0,0 +1,25 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.*;
+import org.cryptomator.cryptolib.common.ReseedingSecureRandom;
+
+import java.security.SecureRandom;
+
+public class CryptorProviderImpl implements CryptorProvider {
+
+ @Override
+ public Scheme scheme() {
+ return Scheme.UVF_DRAFT;
+ }
+
+ @Override
+ public Cryptor provide(Masterkey masterkey, SecureRandom random) {
+ if (masterkey instanceof RevolvingMasterkey) {
+ RevolvingMasterkey revolvingMasterkey = (RevolvingMasterkey) masterkey;
+ return new CryptorImpl(revolvingMasterkey, ReseedingSecureRandom.create(random));
+ } else {
+ throw new IllegalArgumentException("V3 Cryptor requires a RevolvingMasterkey.");
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImpl.java
new file mode 100644
index 0000000..8d7227c
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImpl.java
@@ -0,0 +1,117 @@
+package org.cryptomator.cryptolib.v3;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.api.RevolvingMasterkey;
+
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+
+import static org.cryptomator.cryptolib.v3.Constants.UVF_FILE_EXT;
+
+class DirectoryContentCryptorImpl implements DirectoryContentCryptor {
+
+ private final RevolvingMasterkey masterkey;
+ private final SecureRandom random;
+ private final CryptorImpl cryptor;
+
+ public DirectoryContentCryptorImpl(RevolvingMasterkey masterkey, SecureRandom random, CryptorImpl cryptor) {
+ this.masterkey = masterkey;
+ this.random = random;
+ this.cryptor = cryptor;
+ }
+
+ // DIRECTORY METADATA
+
+ @Override
+ public DirectoryMetadataImpl rootDirectoryMetadata() {
+ byte[] dirId = masterkey.rootDirId();
+ return new DirectoryMetadataImpl(masterkey.firstRevision(), dirId);
+ }
+
+ @Override
+ public DirectoryMetadataImpl newDirectoryMetadata() {
+ byte[] dirId = new byte[32];
+ random.nextBytes(dirId);
+ return new DirectoryMetadataImpl(masterkey.currentRevision(), dirId);
+ }
+
+ @Override
+ public DirectoryMetadataImpl decryptDirectoryMetadata(byte[] ciphertext) throws AuthenticationFailedException {
+ if (ciphertext.length != 128) {
+ throw new IllegalArgumentException("Invalid dir.uvf length: " + ciphertext.length);
+ }
+ int headerSize = cryptor.fileHeaderCryptor().headerSize();
+ ByteBuffer buffer = ByteBuffer.wrap(ciphertext);
+ ByteBuffer headerBuf = buffer.duplicate();
+ headerBuf.position(0).limit(headerSize);
+ ByteBuffer contentBuf = buffer.duplicate();
+ contentBuf.position(headerSize);
+ FileHeaderImpl header = cryptor.fileHeaderCryptor().decryptHeader(headerBuf);
+ ByteBuffer plaintext = cryptor.fileContentCryptor().decryptChunk(contentBuf, 0, header, true);
+ assert plaintext.remaining() == 32;
+ byte[] dirId = new byte[32];
+ plaintext.get(dirId);
+ return new DirectoryMetadataImpl(header.getSeedId(), dirId);
+ }
+
+ @Override
+ public byte[] encryptDirectoryMetadata(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ ByteBuffer cleartextBuf = ByteBuffer.wrap(metadataImpl.dirId());
+ FileHeader header = cryptor.fileHeaderCryptor(metadataImpl.seedId()).create();
+ ByteBuffer headerBuf = cryptor.fileHeaderCryptor().encryptHeader(header);
+ ByteBuffer contentBuf = cryptor.fileContentCryptor().encryptChunk(cleartextBuf, 0, header);
+ byte[] result = new byte[headerBuf.remaining() + contentBuf.remaining()];
+ headerBuf.get(result, 0, headerBuf.remaining());
+ contentBuf.get(result, headerBuf.limit(), contentBuf.remaining());
+ return result;
+ }
+
+ // DIR PATH
+
+ @Override
+ public String dirPath(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor(metadataImpl.seedId());
+ String dirIdStr = fileNameCryptor.hashDirectoryId(metadataImpl.dirId());
+ assert dirIdStr.length() == 32;
+ return "d/" + dirIdStr.substring(0, 2) + "/" + dirIdStr.substring(2);
+ }
+
+ // FILE NAMES
+
+ @Override
+ public Decrypting fileNameDecryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor(metadataImpl.seedId());
+ return ciphertextAndExt -> {
+ String ciphertext = removeExtension(ciphertextAndExt);
+ return fileNameCryptor.decryptFilename(BaseEncoding.base64Url(), ciphertext, dirId);
+ };
+ }
+
+ @Override
+ public Encrypting fileNameEncryptor(DirectoryMetadata directoryMetadata) {
+ DirectoryMetadataImpl metadataImpl = DirectoryMetadataImpl.cast(directoryMetadata);
+ byte[] dirId = metadataImpl.dirId();
+ FileNameCryptorImpl fileNameCryptor = cryptor.fileNameCryptor(metadataImpl.seedId());
+ return plaintext -> {
+ String ciphertext = fileNameCryptor.encryptFilename(BaseEncoding.base64Url(), plaintext, dirId);
+ return ciphertext + UVF_FILE_EXT;
+ };
+ }
+
+ private static String removeExtension(String filename) {
+ if (filename.endsWith(UVF_FILE_EXT)) {
+ return filename.substring(0, filename.length() - UVF_FILE_EXT.length());
+ } else {
+ throw new IllegalArgumentException("Not a " + UVF_FILE_EXT + " file: " + filename);
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/DirectoryMetadataImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/DirectoryMetadataImpl.java
new file mode 100644
index 0000000..ee0b908
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/DirectoryMetadataImpl.java
@@ -0,0 +1,31 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+
+class DirectoryMetadataImpl implements DirectoryMetadata {
+
+ private final int seedId;
+ private final byte[] dirId;
+
+ public DirectoryMetadataImpl(int seedId, byte[] dirId) {
+ this.seedId = seedId;
+ this.dirId = dirId;
+ }
+
+ static DirectoryMetadataImpl cast(DirectoryMetadata metadata) {
+ if (metadata instanceof DirectoryMetadataImpl) {
+ return (DirectoryMetadataImpl) metadata;
+ } else {
+ throw new IllegalArgumentException("Unsupported metadata type " + metadata.getClass());
+ }
+ }
+
+ public byte[] dirId() {
+ return dirId;
+ }
+
+ public int seedId() {
+ return seedId;
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/FileContentCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/FileContentCryptorImpl.java
new file mode 100644
index 0000000..77980f5
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/FileContentCryptorImpl.java
@@ -0,0 +1,149 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.FileContentCryptor;
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.common.CipherSupplier;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.ObjectPool;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.SecureRandom;
+
+import static org.cryptomator.cryptolib.v3.Constants.CHUNK_SIZE;
+import static org.cryptomator.cryptolib.v3.Constants.GCM_NONCE_SIZE;
+import static org.cryptomator.cryptolib.v3.Constants.GCM_TAG_SIZE;
+import static org.cryptomator.cryptolib.v3.Constants.PAYLOAD_SIZE;
+
+class FileContentCryptorImpl implements FileContentCryptor {
+
+ private final SecureRandom random;
+
+ FileContentCryptorImpl(SecureRandom random) {
+ this.random = random;
+ }
+
+ @Override
+ public boolean canSkipAuthentication() {
+ return false;
+ }
+
+ @Override
+ public int cleartextChunkSize() {
+ return PAYLOAD_SIZE;
+ }
+
+ @Override
+ public int ciphertextChunkSize() {
+ return CHUNK_SIZE;
+ }
+
+ @Override
+ public ByteBuffer encryptChunk(ByteBuffer cleartextChunk, long chunkNumber, FileHeader header) {
+ ByteBuffer ciphertextChunk = ByteBuffer.allocate(CHUNK_SIZE);
+ encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, header);
+ ciphertextChunk.flip();
+ return ciphertextChunk;
+ }
+
+ @Override
+ public void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, FileHeader header) {
+ if (cleartextChunk.remaining() < 0 || cleartextChunk.remaining() > PAYLOAD_SIZE) {
+ throw new IllegalArgumentException("Invalid cleartext chunk size: " + cleartextChunk.remaining() + ", expected range [1, " + PAYLOAD_SIZE + "]");
+ }
+ if (ciphertextChunk.remaining() < CHUNK_SIZE) {
+ throw new IllegalArgumentException("Invalid ciphertext chunk size: " + ciphertextChunk.remaining() + ", must fit up to " + CHUNK_SIZE + " bytes.");
+ }
+ FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
+ encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerImpl.getNonce(), headerImpl.getContentKey());
+ }
+
+ @Override
+ public ByteBuffer decryptChunk(ByteBuffer ciphertextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException {
+ // FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE is required to fix a bug in Android API level pre 29, see https://issuetracker.google.com/issues/197534888 and #35
+ ByteBuffer cleartextChunk = ByteBuffer.allocate(PAYLOAD_SIZE + GCM_TAG_SIZE);
+ decryptChunk(ciphertextChunk, cleartextChunk, chunkNumber, header, authenticate);
+ cleartextChunk.flip();
+ return cleartextChunk;
+ }
+
+ @Override
+ public void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, FileHeader header, boolean authenticate) throws AuthenticationFailedException {
+ if (ciphertextChunk.remaining() < GCM_NONCE_SIZE + GCM_TAG_SIZE || ciphertextChunk.remaining() > CHUNK_SIZE) {
+ throw new IllegalArgumentException("Invalid ciphertext chunk size: " + ciphertextChunk.remaining() + ", expected range [" + (GCM_NONCE_SIZE + GCM_TAG_SIZE) + ", " + CHUNK_SIZE + "]");
+ }
+ if (cleartextChunk.remaining() < PAYLOAD_SIZE) {
+ throw new IllegalArgumentException("Invalid cleartext chunk size: " + cleartextChunk.remaining() + ", must fit up to " + PAYLOAD_SIZE + " bytes.");
+ }
+ if (!authenticate) {
+ throw new UnsupportedOperationException("authenticate can not be false");
+ }
+ FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
+ decryptChunk(ciphertextChunk, cleartextChunk, chunkNumber, headerImpl.getNonce(), headerImpl.getContentKey());
+ }
+
+ // visible for testing
+ void encryptChunk(ByteBuffer cleartextChunk, ByteBuffer ciphertextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) {
+ try (DestroyableSecretKey fk = fileKey.copy()) {
+ // nonce:
+ byte[] nonce = new byte[GCM_NONCE_SIZE];
+ random.nextBytes(nonce);
+
+ // payload:
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
+ cipher.get().updateAAD(chunkNumberBigEndian);
+ cipher.get().updateAAD(headerNonce);
+ ciphertextChunk.put(nonce);
+ assert ciphertextChunk.remaining() >= cipher.get().getOutputSize(cleartextChunk.remaining());
+ cipher.get().doFinal(cleartextChunk, ciphertextChunk);
+ }
+ } catch (ShortBufferException e) {
+ throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unexpected exception during GCM encryption.", e);
+ }
+ }
+
+ // visible for testing
+ void decryptChunk(ByteBuffer ciphertextChunk, ByteBuffer cleartextChunk, long chunkNumber, byte[] headerNonce, DestroyableSecretKey fileKey) throws AuthenticationFailedException {
+ assert ciphertextChunk.remaining() >= GCM_NONCE_SIZE + GCM_TAG_SIZE;
+
+ try (DestroyableSecretKey fk = fileKey.copy()) {
+ // nonce:
+ final byte[] nonce = new byte[GCM_NONCE_SIZE];
+ ciphertextChunk.get(nonce, 0, GCM_NONCE_SIZE);
+
+ // payload:
+ final ByteBuffer payloadBuf = ciphertextChunk.duplicate();
+ assert payloadBuf.remaining() >= GCM_TAG_SIZE;
+
+ // payload:
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(fk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ final byte[] chunkNumberBigEndian = longToBigEndianByteArray(chunkNumber);
+ cipher.get().updateAAD(chunkNumberBigEndian);
+ cipher.get().updateAAD(headerNonce);
+ assert cleartextChunk.remaining() >= cipher.get().getOutputSize(payloadBuf.remaining());
+ cipher.get().doFinal(payloadBuf, cleartextChunk);
+ }
+ } catch (AEADBadTagException e) {
+ throw new AuthenticationFailedException("Content tag mismatch.", e);
+ } catch (ShortBufferException e) {
+ throw new IllegalStateException("Buffer allocated for reported output size apparently not big enough.", e);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
+ }
+ }
+
+ private byte[] longToBigEndianByteArray(long n) {
+ return ByteBuffer.allocate(Long.BYTES).order(ByteOrder.BIG_ENDIAN).putLong(n).array();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImpl.java
new file mode 100644
index 0000000..1bc10bc
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImpl.java
@@ -0,0 +1,130 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.*;
+import org.cryptomator.cryptolib.common.CipherSupplier;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.ObjectPool;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.GCMParameterSpec;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import static org.cryptomator.cryptolib.v3.Constants.GCM_TAG_SIZE;
+
+class FileHeaderCryptorImpl implements FileHeaderCryptor {
+
+ private static final byte[] KDF_CONTEXT = "fileHeader".getBytes(StandardCharsets.US_ASCII);
+
+ private final RevolvingMasterkey masterkey;
+ private final SecureRandom random;
+ private final int revision;
+
+ FileHeaderCryptorImpl(RevolvingMasterkey masterkey, SecureRandom random, int revision) {
+ this.masterkey = masterkey;
+ this.random = random;
+ this.revision = revision;
+ }
+
+ @Override
+ public FileHeaderImpl create() {
+ byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
+ random.nextBytes(nonce);
+ byte[] contentKeyBytes = new byte[FileHeaderImpl.CONTENT_KEY_LEN];
+ random.nextBytes(contentKeyBytes);
+ DestroyableSecretKey contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
+ return new FileHeaderImpl(revision, nonce, contentKey);
+ }
+
+ @Override
+ public int headerSize() {
+ return FileHeaderImpl.SIZE;
+ }
+
+ @Override
+ public ByteBuffer encryptHeader(FileHeader header) {
+ FileHeaderImpl headerImpl = FileHeaderImpl.cast(header);
+ try (DestroyableSecretKey headerKey = masterkey.subKey(headerImpl.getSeedId(), 32, KDF_CONTEXT, "AES")) {
+ ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE);
+
+ // general header:
+ result.put(Constants.UVF_MAGIC_BYTES);
+ result.order(ByteOrder.BIG_ENDIAN).putInt(headerImpl.getSeedId());
+ ByteBuffer generalHeaderBuf = result.duplicate();
+ generalHeaderBuf.position(0).limit(FileHeaderImpl.UVF_GENERAL_HEADERS_LEN);
+
+ // format-specific header:
+ result.put(headerImpl.getNonce());
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(headerKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce()))) {
+ cipher.get().updateAAD(generalHeaderBuf);
+ ByteBuffer payloadCleartextBuf = ByteBuffer.wrap(headerImpl.getContentKey().getEncoded());
+ int encrypted = cipher.get().doFinal(payloadCleartextBuf, result);
+ assert encrypted == FileHeaderImpl.CONTENT_KEY_LEN + FileHeaderImpl.TAG_LEN;
+ }
+ result.flip();
+ return result;
+ } catch (ShortBufferException e) {
+ throw new IllegalStateException("Result buffer too small for encrypted header payload.", e);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unexpected exception during GCM encryption.", e);
+ }
+ }
+
+ @Override
+ public FileHeaderImpl decryptHeader(ByteBuffer ciphertextHeaderBuf) throws AuthenticationFailedException {
+ if (ciphertextHeaderBuf.remaining() < FileHeaderImpl.SIZE) {
+ throw new IllegalArgumentException("Malformed ciphertext header");
+ }
+ ByteBuffer buf = ciphertextHeaderBuf.duplicate();
+
+ // general header:
+ byte[] magicBytes = new byte[Constants.UVF_MAGIC_BYTES.length];
+ buf.get(magicBytes);
+ if (!Arrays.equals(Constants.UVF_MAGIC_BYTES, magicBytes)) {
+ throw new IllegalArgumentException("Not an UVF0 file");
+ }
+ int seedId = buf.order(ByteOrder.BIG_ENDIAN).getInt();
+ ByteBuffer generalHeaderBuf = buf.duplicate();
+ generalHeaderBuf.position(0).limit(FileHeaderImpl.UVF_GENERAL_HEADERS_LEN);
+
+ // format-specific header:
+ byte[] nonce = new byte[FileHeaderImpl.NONCE_LEN];
+ buf.position(FileHeaderImpl.NONCE_POS);
+ buf.get(nonce);
+ byte[] ciphertextAndTag = new byte[FileHeaderImpl.CONTENT_KEY_LEN + FileHeaderImpl.TAG_LEN];
+ buf.position(FileHeaderImpl.CONTENT_KEY_POS);
+ buf.get(ciphertextAndTag);
+
+ // FileHeaderImpl.Payload.SIZE + GCM_TAG_SIZE is required to fix a bug in Android API level pre 29, see https://issuetracker.google.com/issues/197534888 and #24
+ ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.CONTENT_KEY_LEN + GCM_TAG_SIZE);
+ try (DestroyableSecretKey headerKey = masterkey.subKey(seedId, 32, KDF_CONTEXT, "AES")) {
+ // decrypt payload:
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.decryptionCipher(headerKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce))) {
+ cipher.get().updateAAD(generalHeaderBuf);
+ int decrypted = cipher.get().doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf);
+ assert decrypted == FileHeaderImpl.CONTENT_KEY_LEN;
+ }
+ payloadCleartextBuf.flip();
+ byte[] contentKeyBytes = new byte[FileHeaderImpl.CONTENT_KEY_LEN];
+ payloadCleartextBuf.get(contentKeyBytes);
+ DestroyableSecretKey contentKey = new DestroyableSecretKey(contentKeyBytes, Constants.CONTENT_ENC_ALG);
+ return new FileHeaderImpl(seedId, nonce, contentKey);
+ } catch (AEADBadTagException e) {
+ throw new AuthenticationFailedException("Header tag mismatch.", e);
+ } catch (ShortBufferException e) {
+ throw new IllegalStateException("Result buffer too small for decrypted header payload.", e);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unexpected exception during GCM decryption.", e);
+ } finally {
+ Arrays.fill(payloadCleartextBuf.array(), (byte) 0x00);
+ }
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderImpl.java
new file mode 100644
index 0000000..40dccdb
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/FileHeaderImpl.java
@@ -0,0 +1,72 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+
+import javax.security.auth.Destroyable;
+
+class FileHeaderImpl implements FileHeader, Destroyable {
+
+ static final int UVF_GENERAL_HEADERS_LEN = Constants.UVF_MAGIC_BYTES.length + Integer.BYTES;
+ static final int NONCE_POS = 8;
+ static final int NONCE_LEN = Constants.GCM_NONCE_SIZE;
+ static final int CONTENT_KEY_POS = NONCE_POS + NONCE_LEN; // 20
+ static final int CONTENT_KEY_LEN = 32;
+ static final int TAG_POS = CONTENT_KEY_POS + CONTENT_KEY_LEN; // 52
+ static final int TAG_LEN = Constants.GCM_TAG_SIZE;
+ static final int SIZE = UVF_GENERAL_HEADERS_LEN + NONCE_LEN + CONTENT_KEY_LEN + TAG_LEN;
+
+ private final int seedId;
+ private final byte[] nonce;
+ private final DestroyableSecretKey contentKey;
+
+ FileHeaderImpl(int seedId, byte[] nonce, DestroyableSecretKey contentKey) {
+ if (nonce.length != NONCE_LEN) {
+ throw new IllegalArgumentException("Invalid nonce length. (was: " + nonce.length + ", required: " + NONCE_LEN + ")");
+ }
+ this.seedId = seedId;
+ this.nonce = nonce;
+ this.contentKey = contentKey;
+ }
+
+ static FileHeaderImpl cast(FileHeader header) {
+ if (header instanceof FileHeaderImpl) {
+ return (FileHeaderImpl) header;
+ } else {
+ throw new IllegalArgumentException("Unsupported header type " + header.getClass());
+ }
+ }
+
+ public int getSeedId() {
+ return seedId;
+ }
+
+ public byte[] getNonce() {
+ return nonce;
+ }
+
+ public DestroyableSecretKey getContentKey() {
+ return contentKey;
+ }
+
+ @Override
+ public long getReserved() {
+ return 0;
+ }
+
+ @Override
+ public void setReserved(long reserved) {
+ /* noop */
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return contentKey.isDestroyed();
+ }
+
+ @Override
+ public void destroy() {
+ contentKey.destroy();
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java
new file mode 100644
index 0000000..bc80a34
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java
@@ -0,0 +1,71 @@
+package org.cryptomator.cryptolib.v3;
+
+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.api.RevolvingMasterkey;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.MacSupplier;
+import org.cryptomator.cryptolib.common.MessageDigestSupplier;
+import org.cryptomator.cryptolib.common.ObjectPool;
+import org.cryptomator.siv.SivMode;
+import org.cryptomator.siv.UnauthenticCiphertextException;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+class FileNameCryptorImpl implements FileNameCryptor {
+
+ private static final BaseEncoding BASE32 = BaseEncoding.base32();
+ private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new);
+
+ private final DestroyableSecretKey sivKey;
+ private final DestroyableSecretKey hmacKey;
+
+ /**
+ * Create a file name encryption/decryption tool for a certain masterkey revision.
+ * @param masterkey The masterkey from which to derive subkeys
+ * @param revision Which masterkey revision to use
+ * @throws IllegalArgumentException If no subkey could be derived for the given revision
+ */
+ FileNameCryptorImpl(RevolvingMasterkey masterkey, int revision) throws IllegalArgumentException {
+ this.sivKey = masterkey.subKey(revision, 64, "siv".getBytes(StandardCharsets.US_ASCII), "AES");
+ this.hmacKey = masterkey.subKey(revision, 64, "hmac".getBytes(StandardCharsets.US_ASCII), "HMAC");
+ }
+
+ @Override
+ public String hashDirectoryId(byte[] cleartextDirectoryId) {
+ try (DestroyableSecretKey key = this.hmacKey.copy();
+ ObjectPool.Lease hmacSha256 = MacSupplier.HMAC_SHA256.keyed(key)) {
+ byte[] hash = hmacSha256.get().doFinal(cleartextDirectoryId);
+ return BASE32.encode(hash, 0, 20); // only use first 160 bits
+ }
+ }
+
+ @Override
+ public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) {
+ try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease siv = AES_SIV.get()) {
+ byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
+ byte[] encryptedBytes = siv.get().encrypt(key, cleartextBytes, associatedData);
+ return encoding.encode(encryptedBytes);
+ }
+ }
+
+ @Override
+ public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
+ try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease siv = AES_SIV.get()) {
+ byte[] encryptedBytes = encoding.decode(ciphertextName);
+ byte[] cleartextBytes = siv.get().decrypt(key, encryptedBytes, associatedData);
+ return new String(cleartextBytes, UTF_8);
+ } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) {
+ throw new AuthenticationFailedException("Invalid Ciphertext.", e);
+ }
+ }
+
+}
diff --git a/src/main/java9/module-info.java b/src/main/java9/module-info.java
index 512fb83..ad82dde 100644
--- a/src/main/java9/module-info.java
+++ b/src/main/java9/module-info.java
@@ -24,5 +24,5 @@
uses CryptorProvider;
provides CryptorProvider
- with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl;
+ with org.cryptomator.cryptolib.v1.CryptorProviderImpl, org.cryptomator.cryptolib.v2.CryptorProviderImpl, org.cryptomator.cryptolib.v3.CryptorProviderImpl;
}
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider b/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider
index 4e7fe58..cbbe5e5 100644
--- a/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider
+++ b/src/main/resources/META-INF/services/org.cryptomator.cryptolib.api.CryptorProvider
@@ -1,2 +1,3 @@
org.cryptomator.cryptolib.v1.CryptorProviderImpl
-org.cryptomator.cryptolib.v2.CryptorProviderImpl
\ No newline at end of file
+org.cryptomator.cryptolib.v2.CryptorProviderImpl
+org.cryptomator.cryptolib.v3.CryptorProviderImpl
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
index f846629..7f18ebc 100644
--- a/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/api/CryptoLibIntegrationTest.java
@@ -30,11 +30,27 @@
public class CryptoLibIntegrationTest {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
+ private static final String UVF_PAYLOAD = "{\n" +
+ " \"fileFormat\": \"AES-256-GCM-32k\",\n" +
+ " \"nameFormat\": \"AES-SIV-512-B64URL\",\n" +
+ " \"seeds\": {\n" +
+ " \"HDm38g\": \"ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs\",\n" +
+ " \"gBryKw\": \"PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0\",\n" +
+ " \"QBsJFg\": \"Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y\"\n" +
+ " },\n" +
+ " \"initialSeed\": \"HDm38i\",\n" +
+ " \"latestSeed\": \"QBsJFo\",\n" +
+ " \"kdf\": \"HKDF-SHA512\",\n" +
+ " \"kdfSalt\": \"NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D-6oiIjr8\",\n" +
+ " \"org.example.customfield\": 42\n" +
+ "}";
private static Stream getCryptors() {
return Stream.of(
CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK),
- CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK)
+ CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(Masterkey.generate(RANDOM_MOCK), RANDOM_MOCK),
+ CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT).provide(UVFMasterkey.fromDecryptedPayload(UVF_PAYLOAD), RANDOM_MOCK)
+
);
}
diff --git a/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java
new file mode 100644
index 0000000..e995df8
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java
@@ -0,0 +1,60 @@
+package org.cryptomator.cryptolib.api;
+
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class UVFMasterkeyTest {
+
+ @Test
+ public void testFromDecryptedPayload() {
+ String json = "{\n" +
+ " \"fileFormat\": \"AES-256-GCM-32k\",\n" +
+ " \"nameFormat\": \"AES-SIV-512-B64URL\",\n" +
+ " \"seeds\": {\n" +
+ " \"HDm38i\": \"ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs\",\n" +
+ " \"gBryKw\": \"PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0\",\n" +
+ " \"QBsJFo\": \"Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y\"\n" +
+ " },\n" +
+ " \"initialSeed\": \"HDm38i\",\n" +
+ " \"latestSeed\": \"QBsJFo\",\n" +
+ " \"kdf\": \"HKDF-SHA512\",\n" +
+ " \"kdfSalt\": \"NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D-6oiIjr8\",\n" +
+ " \"org.example.customfield\": 42\n" +
+ "}";
+ UVFMasterkey masterkey = UVFMasterkey.fromDecryptedPayload(json);
+
+ Assertions.assertEquals(473544690, masterkey.initialSeed);
+ Assertions.assertEquals(1075513622, masterkey.latestSeed);
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D-6oiIjr8"), masterkey.kdfSalt);
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs"), masterkey.seeds.get(473544690));
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y"), masterkey.seeds.get(1075513622));
+ }
+
+ @Test
+ public void testSubkey() {
+ Map seeds = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ byte[] kdfSalt = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ try (UVFMasterkey masterkey = new UVFMasterkey(seeds, kdfSalt, -1540072521, -1540072521)) {
+ try (DestroyableSecretKey subkey = masterkey.subKey(-1540072521, 32, "fileHeader".getBytes(StandardCharsets.US_ASCII), "AES")) {
+ Assertions.assertEquals("PwnW2t/pK9dmzc+GTLdBSaB8ilcwsTq4sYOeiyo3cpU=", Base64.getEncoder().encodeToString(subkey.getEncoded()));
+ }
+ }
+ }
+
+ @Test
+ public void testRootDirId() {
+ Map seeds = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ byte[] kdfSalt = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ try (UVFMasterkey masterkey = new UVFMasterkey(seeds, kdfSalt, -1540072521, -1540072521)) {
+ Assertions.assertEquals("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc=", Base64.getEncoder().encodeToString(masterkey.rootDirId()));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java
index acbcf82..34397a4 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/DestroyableSecretKeyTest.java
@@ -14,6 +14,7 @@
import java.util.Arrays;
import java.util.Random;
+@SuppressWarnings("resource")
public class DestroyableSecretKeyTest {
@DisplayName("generate(...)")
diff --git a/src/test/java/org/cryptomator/cryptolib/common/HKDFHelperTest.java b/src/test/java/org/cryptomator/cryptolib/common/HKDFHelperTest.java
new file mode 100644
index 0000000..36f2b2d
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/common/HKDFHelperTest.java
@@ -0,0 +1,69 @@
+package org.cryptomator.cryptolib.common;
+
+import com.google.common.io.BaseEncoding;
+import org.bouncycastle.crypto.digests.SHA256Digest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+public class HKDFHelperTest {
+
+ private static final BaseEncoding HEX = BaseEncoding.base16().ignoreCase();
+
+ @Test
+ @DisplayName("RFC 5869 Test Case 1")
+ public void testCase1() {
+ // https://datatracker.ietf.org/doc/html/rfc5869#appendix-A.1
+ byte[] ikm = HEX.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ byte[] salt = HEX.decode("000102030405060708090a0b0c");
+ byte[] info = HEX.decode("f0f1f2f3f4f5f6f7f8f9");
+
+ byte[] result = HKDFHelper.hkdf(new SHA256Digest(), salt, ikm, info, 42);
+
+ byte[] expectedOkm = HEX.ignoreCase().decode("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865");
+ Assertions.assertArrayEquals(expectedOkm, result);
+ }
+
+ @Test
+ @DisplayName("RFC 5869 Test Case 2")
+ public void testCase2() {
+ // https://datatracker.ietf.org/doc/html/rfc5869#appendix-A.2
+ byte[] ikm = HEX.decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f");
+ byte[] salt = HEX.decode("606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeaf");
+ byte[] info = HEX.decode("b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
+
+ byte[] result = HKDFHelper.hkdf(new SHA256Digest(), salt, ikm, info, 82);
+
+ byte[] expectedOkm = HEX.ignoreCase().decode("b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71cc30c58179ec3e87c14c01d5c1f3434f1d87");
+ Assertions.assertArrayEquals(expectedOkm, result);
+ }
+
+ @Test
+ @DisplayName("RFC 5869 Test Case 3")
+ public void testCase3() {
+ // https://datatracker.ietf.org/doc/html/rfc5869#appendix-A.3
+ byte[] ikm = HEX.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ byte[] salt = new byte[0];
+ byte[] info = new byte[0];
+
+ byte[] result = HKDFHelper.hkdf(new SHA256Digest(), salt, ikm, info, 42);
+
+ byte[] expectedOkm = HEX.ignoreCase().decode("8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8");
+ Assertions.assertArrayEquals(expectedOkm, result);
+ }
+
+ @Test
+ @DisplayName("Inofficial SHA-512 Test")
+ public void sha512Test() {
+ // https://github.com/patrickfav/hkdf/blob/60152fff852506a1b46f730b14d1b8f8ff69d071/src/test/java/at/favre/lib/hkdf/RFC5869TestCases.java#L116-L124
+ byte[] ikm = HEX.decode("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b");
+ byte[] salt = HEX.decode("000102030405060708090a0b0c");
+ byte[] info = HEX.decode("f0f1f2f3f4f5f6f7f8f9");
+
+ byte[] result = HKDFHelper.hkdfSha512(salt, ikm, info, 42);
+
+ byte[] expectedOkm = HEX.ignoreCase().decode("832390086CDA71FB47625BB5CEB168E4C8E26A1A16ED34D9FC7FE92C1481579338DA362CB8D9F925D7CB");
+ Assertions.assertArrayEquals(expectedOkm, result);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java
index e35d510..b587e75 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyFileAccessTest.java
@@ -1,10 +1,7 @@
package org.cryptomator.cryptolib.common;
import com.google.common.io.BaseEncoding;
-import org.cryptomator.cryptolib.api.CryptoException;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.cryptolib.api.*;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -30,7 +27,7 @@ public class MasterkeyFileAccessTest {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
private static final byte[] DEFAULT_PEPPER = new byte[0];
- private Masterkey key = new Masterkey(new byte[64]);
+ private PerpetualMasterkey key = new PerpetualMasterkey(new byte[64]);
private MasterkeyFile keyFile = new MasterkeyFile();
private MasterkeyFileAccess masterkeyFileAccess = Mockito.spy(new MasterkeyFileAccess(DEFAULT_PEPPER, RANDOM_MOCK));
@@ -93,7 +90,7 @@ public void testChangePassphraseWithRawBytes() throws CryptoException, IOExcepti
public void testLoad() throws IOException {
InputStream in = new ByteArrayInputStream(serializedKeyFile);
- Masterkey loaded = masterkeyFileAccess.load(in, "asd");
+ PerpetualMasterkey loaded = masterkeyFileAccess.load(in, "asd");
Assertions.assertArrayEquals(key.getEncoded(), loaded.getEncoded());
}
@@ -203,7 +200,7 @@ public void testPersistAndLoad(@TempDir Path tmpDir) throws IOException, Masterk
Path masterkeyFile = tmpDir.resolve("masterkey.cryptomator");
masterkeyFileAccess.persist(key, masterkeyFile, "asd");
- Masterkey loaded = masterkeyFileAccess.load(masterkeyFile, "asd");
+ PerpetualMasterkey loaded = masterkeyFileAccess.load(masterkeyFile, "asd");
Assertions.assertArrayEquals(key.getEncoded(), loaded.getEncoded());
}
diff --git a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java
index 4b0a2a0..d25121a 100644
--- a/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/common/MasterkeyTest.java
@@ -1,6 +1,7 @@
package org.cryptomator.cryptolib.common;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -13,7 +14,7 @@
public class MasterkeyTest {
private byte[] raw;
- private Masterkey masterkey;
+ private PerpetualMasterkey masterkey;
@BeforeEach
public void setup() {
@@ -21,7 +22,7 @@ public void setup() {
for (byte b=0; b cryptor.fileNameCryptor(revision));
+ }
+ }
+
@Test
public void testExplicitDestruction() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
cryptor.destroy();
Mockito.verify(masterkey).destroy();
@@ -64,7 +74,7 @@ public void testExplicitDestruction() {
@Test
public void testImplicitDestruction() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
Assertions.assertFalse(cryptor.isDestroyed());
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java
index 77096c8..84ac1cb 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorProviderImplTest.java
@@ -8,7 +8,9 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v1;
+import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -22,9 +24,9 @@ public class CryptorProviderImplTest {
@Test
public void testProvide() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
- CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK);
- Assertions.assertNotNull(cryptor);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
+ CryptorProvider provider = new CryptorProviderImpl();
+ Assertions.assertInstanceOf(CryptorImpl.class, provider.provide(masterkey, RANDOM_MOCK));
}
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImplTest.java
new file mode 100644
index 0000000..161139d
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v1/DirectoryContentCryptorImplTest.java
@@ -0,0 +1,140 @@
+package org.cryptomator.cryptolib.v1;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+class DirectoryContentCryptorImplTest {
+
+ private static final SecureRandom CSPRNG = new SecureRandom();
+ private static DirectoryContentCryptorImpl dirCryptor;
+
+ @BeforeAll
+ public static void setUp() {
+ byte[] key = new byte[64];
+ Arrays.fill(key, 0, 32, (byte) 0x55); // enc key
+ Arrays.fill(key, 32, 64, (byte) 0x77); // mac key
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(key);
+ dirCryptor = (DirectoryContentCryptorImpl) CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_CTRMAC).provide(masterkey, CSPRNG).directoryContentCryptor();
+ }
+
+ @Test
+ @DisplayName("encrypt and decrypt dir.c9r files")
+ public void encryptAndDecryptDirectoryMetadata() {
+ DirectoryMetadataImpl origMetadata = dirCryptor.newDirectoryMetadata();
+
+ byte[] encryptedMetadata = dirCryptor.encryptDirectoryMetadata(origMetadata);
+ DirectoryMetadataImpl decryptedMetadata = dirCryptor.decryptDirectoryMetadata(encryptedMetadata);
+
+ Assertions.assertArrayEquals(origMetadata.dirId(), decryptedMetadata.dirId());
+ }
+
+ @Test
+ @DisplayName("encrypt WELCOME.rtf in root dir")
+ public void testEncryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Encrypting enc = dirCryptor.fileNameEncryptor(rootDirMetadata);
+ String ciphertext = enc.encrypt("WELCOME.rtf");
+ Assertions.assertEquals("4BwXESMPHMIGXeiyQifg2xBDzblPVdRuU1dy.c9r", ciphertext);
+ }
+
+ @Test
+ @DisplayName("decrypt WELCOME.rtf in root dir")
+ public void testDecryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Decrypting dec = dirCryptor.fileNameDecryptor(rootDirMetadata);
+ String plaintext = dec.decrypt("4BwXESMPHMIGXeiyQifg2xBDzblPVdRuU1dy.c9r");
+ Assertions.assertEquals("WELCOME.rtf", plaintext);
+ }
+
+ @Test
+ @DisplayName("get root dir path")
+ public void testRootDirPath() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ String path = dirCryptor.dirPath(rootDirMetadata);
+ Assertions.assertEquals("d/VL/WEHT553J5DR7OZLRJAYDIWFCXZABOD", path);
+ }
+
+ @Nested
+ @DisplayName("Given a specific dir.c9f file")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithDirectoryMetadata {
+
+ DirectoryMetadataImpl dirC9r;
+ DirectoryContentCryptor.Encrypting enc;
+ DirectoryContentCryptor.Decrypting dec;
+
+ @BeforeAll
+ public void setup() {
+ dirC9r = new DirectoryMetadataImpl("deadbeef-cafe-4bob-beef-decafbadface".getBytes(StandardCharsets.US_ASCII));
+ enc = dirCryptor.fileNameEncryptor(dirC9r);
+ dec = dirCryptor.fileNameDecryptor(dirC9r);
+ }
+
+ @DisplayName("encrypt multiple file names")
+ @ParameterizedTest(name = "fileNameEncryptor.encrypt('{0}') == '{1}'")
+ @CsvSource({
+ "file1.txt, sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r",
+ "file2.txt, hNJmLgIVcneOTeK5E-K_v3Vd9hgb2jJcQA==.c9r",
+ "file3.txt, qjfr-LCwvfTDMjWmR1CwEAcM7cj-IFDVIw==.c9r",
+ "file4.txt, dqL5KkgfQveDBRXl3o6FmSZ87apNzNeiDg==.c9r"
+ })
+ public void testBulkEncryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(ciphertext, enc.encrypt(plaintext));
+ }
+
+ @DisplayName("decrypt multiple file names")
+ @ParameterizedTest(name = "fileNameDecryptor.decrypt('{1}') == '{0}'")
+ @CsvSource({
+ "file1.txt, sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r",
+ "file2.txt, hNJmLgIVcneOTeK5E-K_v3Vd9hgb2jJcQA==.c9r",
+ "file3.txt, qjfr-LCwvfTDMjWmR1CwEAcM7cj-IFDVIw==.c9r",
+ "file4.txt, dqL5KkgfQveDBRXl3o6FmSZ87apNzNeiDg==.c9r"
+ })
+ public void testBulkDecryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(plaintext, dec.decrypt(ciphertext));
+ }
+
+ @Test
+ @DisplayName("decrypt file with invalid extension")
+ public void testDecryptMalformed1() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ dec.decrypt("sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.INVALID");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with unauthentic ciphertext")
+ public void testDecryptMalformed2() {
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ dec.decrypt("INVALID-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r.c9r");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with incorrect dirId")
+ public void testDecryptMalformed3() {
+ DirectoryMetadataImpl differentDirId = new DirectoryMetadataImpl("deadbeef-cafe-4bob-beef-badbadbadbad".getBytes(StandardCharsets.US_ASCII));
+ DirectoryContentCryptor.Decrypting differentDirIdDec = dirCryptor.fileNameDecryptor(differentDirId);
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ differentDirIdDec.decrypt("sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r");
+ });
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java
index d8902e3..d54a49c 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplBenchmark.java
@@ -16,6 +16,7 @@
import javax.crypto.spec.SecretKeySpec;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.openjdk.jmh.annotations.Benchmark;
@@ -40,7 +41,7 @@
public class FileContentCryptorImplBenchmark {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);;
+ private static final PerpetualMasterkey MASTERKEY = new PerpetualMasterkey(new byte[64]);;
private final byte[] headerNonce = new byte[Constants.NONCE_SIZE];
private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE);
private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE);
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
index f4b11bd..8bbfab2 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentCryptorImplTest.java
@@ -12,11 +12,8 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Cryptor;
import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
-import org.cryptomator.cryptolib.common.DestroyableSecretKey;
-import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
-import org.cryptomator.cryptolib.common.SecureRandomMock;
-import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
+import org.cryptomator.cryptolib.common.*;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
@@ -55,7 +52,7 @@ public class FileContentCryptorImplTest {
@BeforeEach
public void setup() {
- Masterkey masterkey = new Masterkey(new byte[64]);
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]));
headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);
fileContentCryptor = new FileContentCryptorImpl(masterkey, RANDOM_MOCK);
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java
index 229cbea..112ab43 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileContentEncryptorBenchmark.java
@@ -15,6 +15,7 @@
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
@@ -40,7 +41,7 @@
public class FileContentEncryptorBenchmark {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
+ private static final PerpetualMasterkey MASTERKEY = new PerpetualMasterkey(new byte[64]);
private CryptorImpl cryptor;
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java
index 5a89bae..44c74f1 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorBenchmark.java
@@ -11,6 +11,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.openjdk.jmh.annotations.Benchmark;
@@ -39,7 +40,7 @@
public class FileHeaderCryptorBenchmark {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
+ private static final PerpetualMasterkey MASTERKEY = new PerpetualMasterkey(new byte[64]);
private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK);
private FileHeader header;
private ByteBuffer validHeaderCiphertextBuf;
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
index 5605700..eda018d 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileHeaderCryptorImplTest.java
@@ -12,6 +12,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -27,7 +28,7 @@ public class FileHeaderCryptorImplTest {
@BeforeEach
public void setup() {
- Masterkey masterkey = new Masterkey(new byte[64]);
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
index 99ef409..088c211 100644
--- a/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v1/FileNameCryptorImplTest.java
@@ -11,6 +11,7 @@
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.siv.UnauthenticCiphertextException;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
@@ -29,7 +30,7 @@ public class FileNameCryptorImplTest {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
- private final Masterkey masterkey = new Masterkey(new byte[64]);
+ private final PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey);
private static Stream filenameGenerator() {
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java b/src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java
index c10d849..834c3ed 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/BenchmarkTest.java
@@ -9,12 +9,14 @@
package org.cryptomator.cryptolib.v2;
import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
+@DisplayName("Benchmark V2 (GCM)")
public class BenchmarkTest {
@Disabled("only on demand")
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
index ea33b48..34b8a0b 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java
@@ -1,20 +1,14 @@
-/*******************************************************************************
- * Copyright (c) 2016 Sebastian Stenzel and others.
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE.txt.
- *
- * Contributors:
- * Sebastian Stenzel - initial API and implementation
- *******************************************************************************/
package org.cryptomator.cryptolib.v2;
-import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mockito;
import java.security.SecureRandom;
@@ -23,11 +17,11 @@ public class CryptorImplTest {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
- private Masterkey masterkey;
+ private PerpetualMasterkey masterkey;
@BeforeEach
public void setup() {
- this.masterkey = new Masterkey(new byte[64]);
+ this.masterkey = new PerpetualMasterkey(new byte[64]);
}
@Test
@@ -51,9 +45,17 @@ public void testGetFileNameCryptor() {
}
}
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 0, 1, 42, 1337})
+ public void testGetFileNameCryptorWithRevisions(int revision) {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> cryptor.fileNameCryptor(revision));
+ }
+ }
+
@Test
public void testExplicitDestruction() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
cryptor.destroy();
Mockito.verify(masterkey).destroy();
@@ -64,7 +66,7 @@ public void testExplicitDestruction() {
@Test
public void testImplicitDestruction() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
Assertions.assertFalse(cryptor.isDestroyed());
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java
index 95a2618..cecf1e4 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorProviderImplTest.java
@@ -8,7 +8,9 @@
*******************************************************************************/
package org.cryptomator.cryptolib.v2;
+import org.cryptomator.cryptolib.api.CryptorProvider;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -23,9 +25,9 @@ public class CryptorProviderImplTest {
@Test
public void testProvide() {
- Masterkey masterkey = Mockito.mock(Masterkey.class);
- CryptorImpl cryptor = new CryptorProviderImpl().provide(masterkey, RANDOM_MOCK);
- Assertions.assertNotNull(cryptor);
+ PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class);
+ CryptorProvider provider = new CryptorProviderImpl();
+ Assertions.assertInstanceOf(CryptorImpl.class, provider.provide(masterkey, RANDOM_MOCK));
}
}
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImplTest.java
new file mode 100644
index 0000000..f8ab676
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v2/DirectoryContentCryptorImplTest.java
@@ -0,0 +1,140 @@
+package org.cryptomator.cryptolib.v2;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+class DirectoryContentCryptorImplTest {
+
+ private static final SecureRandom CSPRNG = new SecureRandom();
+ private static DirectoryContentCryptorImpl dirCryptor;
+
+ @BeforeAll
+ public static void setUp() {
+ byte[] key = new byte[64];
+ Arrays.fill(key, 0, 32, (byte) 0x55); // enc key
+ Arrays.fill(key, 32, 64, (byte) 0x77); // mac key
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(key);
+ dirCryptor = (DirectoryContentCryptorImpl) CryptorProvider.forScheme(CryptorProvider.Scheme.SIV_GCM).provide(masterkey, CSPRNG).directoryContentCryptor();
+ }
+
+ @Test
+ @DisplayName("encrypt and decrypt dir.c9r files")
+ public void encryptAndDecryptDirectoryMetadata() {
+ DirectoryMetadataImpl origMetadata = dirCryptor.newDirectoryMetadata();
+
+ byte[] encryptedMetadata = dirCryptor.encryptDirectoryMetadata(origMetadata);
+ DirectoryMetadataImpl decryptedMetadata = dirCryptor.decryptDirectoryMetadata(encryptedMetadata);
+
+ Assertions.assertArrayEquals(origMetadata.dirId(), decryptedMetadata.dirId());
+ }
+
+ @Test
+ @DisplayName("encrypt WELCOME.rtf in root dir")
+ public void testEncryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Encrypting enc = dirCryptor.fileNameEncryptor(rootDirMetadata);
+ String ciphertext = enc.encrypt("WELCOME.rtf");
+ Assertions.assertEquals("4BwXESMPHMIGXeiyQifg2xBDzblPVdRuU1dy.c9r", ciphertext);
+ }
+
+ @Test
+ @DisplayName("decrypt WELCOME.rtf in root dir")
+ public void testDecryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Decrypting dec = dirCryptor.fileNameDecryptor(rootDirMetadata);
+ String plaintext = dec.decrypt("4BwXESMPHMIGXeiyQifg2xBDzblPVdRuU1dy.c9r");
+ Assertions.assertEquals("WELCOME.rtf", plaintext);
+ }
+
+ @Test
+ @DisplayName("get root dir path")
+ public void testRootDirPath() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ String path = dirCryptor.dirPath(rootDirMetadata);
+ Assertions.assertEquals("d/VL/WEHT553J5DR7OZLRJAYDIWFCXZABOD", path);
+ }
+
+ @Nested
+ @DisplayName("Given a specific dir.c9f file")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithDirectoryMetadata {
+
+ DirectoryMetadataImpl dirC9r;
+ DirectoryContentCryptor.Encrypting enc;
+ DirectoryContentCryptor.Decrypting dec;
+
+ @BeforeAll
+ public void setup() {
+ dirC9r = new DirectoryMetadataImpl("deadbeef-cafe-4bob-beef-decafbadface".getBytes(StandardCharsets.US_ASCII));
+ enc = dirCryptor.fileNameEncryptor(dirC9r);
+ dec = dirCryptor.fileNameDecryptor(dirC9r);
+ }
+
+ @DisplayName("encrypt multiple file names")
+ @ParameterizedTest(name = "fileNameEncryptor.encrypt('{0}') == '{1}'")
+ @CsvSource({
+ "file1.txt, sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r",
+ "file2.txt, hNJmLgIVcneOTeK5E-K_v3Vd9hgb2jJcQA==.c9r",
+ "file3.txt, qjfr-LCwvfTDMjWmR1CwEAcM7cj-IFDVIw==.c9r",
+ "file4.txt, dqL5KkgfQveDBRXl3o6FmSZ87apNzNeiDg==.c9r"
+ })
+ public void testBulkEncryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(ciphertext, enc.encrypt(plaintext));
+ }
+
+ @DisplayName("decrypt multiple file names")
+ @ParameterizedTest(name = "fileNameDecryptor.decrypt('{1}') == '{0}'")
+ @CsvSource({
+ "file1.txt, sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r",
+ "file2.txt, hNJmLgIVcneOTeK5E-K_v3Vd9hgb2jJcQA==.c9r",
+ "file3.txt, qjfr-LCwvfTDMjWmR1CwEAcM7cj-IFDVIw==.c9r",
+ "file4.txt, dqL5KkgfQveDBRXl3o6FmSZ87apNzNeiDg==.c9r"
+ })
+ public void testBulkDecryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(plaintext, dec.decrypt(ciphertext));
+ }
+
+ @Test
+ @DisplayName("decrypt file with invalid extension")
+ public void testDecryptMalformed1() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ dec.decrypt("sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.INVALID");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with unauthentic ciphertext")
+ public void testDecryptMalformed2() {
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ dec.decrypt("INVALID-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r.c9r");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with incorrect dirId")
+ public void testDecryptMalformed3() {
+ DirectoryMetadataImpl differentDirId = new DirectoryMetadataImpl("deadbeef-cafe-4bob-beef-badbadbadbad".getBytes(StandardCharsets.US_ASCII));
+ DirectoryContentCryptor.Decrypting differentDirIdDec = dirCryptor.fileNameDecryptor(differentDirId);
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ differentDirIdDec.decrypt("sL-e8HOmmqdyIJspqfB0P6zXxoBGHZw9XQ==.c9r");
+ });
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
index 10b1e69..dcbabc0 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java
@@ -9,14 +9,14 @@
package org.cryptomator.cryptolib.v2;
import com.google.common.io.BaseEncoding;
-import org.cryptomator.cryptolib.common.CipherSupplier;
-import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
-import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
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.api.PerpetualMasterkey;
+import org.cryptomator.cryptolib.common.CipherSupplier;
+import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.common.GcmTestHelper;
import org.cryptomator.cryptolib.common.ObjectPool;
import org.cryptomator.cryptolib.common.SecureRandomMock;
@@ -64,7 +64,7 @@ public class FileContentCryptorImplTest {
@BeforeEach
public void setup() {
- Masterkey masterkey = new Masterkey(new byte[64]);
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
header = new FileHeaderImpl(new byte[FileHeaderImpl.NONCE_LEN], new FileHeaderImpl.Payload(-1, new byte[FileHeaderImpl.Payload.CONTENT_KEY_LEN]));
headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG);
fileContentCryptor = new FileContentCryptorImpl(CSPRNG);
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java
index c64ee04..65a95d1 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentEncryptorBenchmark.java
@@ -15,6 +15,7 @@
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
@@ -40,7 +41,7 @@
public class FileContentEncryptorBenchmark {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
+ private static final PerpetualMasterkey MASTERKEY = new PerpetualMasterkey(new byte[64]);
private CryptorImpl cryptor;
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java
index 1d2cd4b..541e41f 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java
@@ -11,6 +11,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.DestroyableSecretKey;
import org.cryptomator.cryptolib.common.SecureRandomMock;
import org.openjdk.jmh.annotations.Benchmark;
@@ -39,7 +40,7 @@
public class FileHeaderCryptorBenchmark {
private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
- private static final Masterkey MASTERKEY = new Masterkey(new byte[64]);
+ private static final PerpetualMasterkey MASTERKEY = new PerpetualMasterkey(new byte[64]);
private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK);
private ByteBuffer validHeaderCiphertextBuf;
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
index baea1a5..5b729e2 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java
@@ -12,6 +12,7 @@
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.FileHeader;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.cryptolib.common.CipherSupplier;
import org.cryptomator.cryptolib.common.GcmTestHelper;
import org.cryptomator.cryptolib.common.ObjectPool;
@@ -32,7 +33,7 @@ public class FileHeaderCryptorImplTest {
@BeforeEach
public void setup() {
- Masterkey masterkey = new Masterkey(new byte[64]);
+ PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK);
// reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
index d808f89..4069392 100644
--- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
+++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java
@@ -11,6 +11,7 @@
import com.google.common.io.BaseEncoding;
import org.cryptomator.cryptolib.api.AuthenticationFailedException;
import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
import org.cryptomator.siv.UnauthenticCiphertextException;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
@@ -30,7 +31,7 @@ public class FileNameCryptorImplTest {
private static final BaseEncoding BASE32 = BaseEncoding.base32();
- private final Masterkey masterkey = new Masterkey(new byte[64]);
+ private final PerpetualMasterkey masterkey = new PerpetualMasterkey(new byte[64]);
private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey);
private static Stream filenameGenerator() {
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/BenchmarkTest.java b/src/test/java/org/cryptomator/cryptolib/v3/BenchmarkTest.java
new file mode 100644
index 0000000..714488d
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/BenchmarkTest.java
@@ -0,0 +1,31 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+@DisplayName("Benchmark V3 (UVF)")
+public class BenchmarkTest {
+
+ @Disabled("only on demand")
+ @Test
+ public void runBenchmarks() throws RunnerException {
+ // Taken from http://stackoverflow.com/a/30486197/4014509:
+ Options opt = new OptionsBuilder()
+ // Specify which benchmarks to run
+ .include(getClass().getPackage().getName() + ".*Benchmark.*")
+ // Set the following options as needed
+ .threads(2).forks(1) //
+ .shouldFailOnError(true).shouldDoGC(true)
+ // .jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining")
+ // .addProfiler(WinPerfAsmProfiler.class)
+ .build();
+
+ new Runner(opt).run();
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java
new file mode 100644
index 0000000..194ce41
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java
@@ -0,0 +1,83 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+public class CryptorImplTest {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+
+ private UVFMasterkey masterkey;
+
+ @BeforeEach
+ public void setup() {
+ masterkey = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+ }
+
+ @Test
+ public void testGetFileContentCryptor() {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertInstanceOf(FileContentCryptorImpl.class, cryptor.fileContentCryptor());
+ }
+ }
+
+ @Test
+ public void testGetFileHeaderCryptor() {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertInstanceOf(FileHeaderCryptorImpl.class, cryptor.fileHeaderCryptor());
+ }
+ }
+
+ @Test
+ public void testGetFileNameCryptor() {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertThrows(UnsupportedOperationException.class, cryptor::fileNameCryptor);
+ }
+ }
+
+ @Test
+ public void testGetFileNameCryptorWithInvalidRevisions() {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> cryptor.fileNameCryptor(0xBAD5EED));
+ }
+ }
+
+ @Test
+ public void testGetFileNameCryptorWithCorrectRevisions() {
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertInstanceOf(FileNameCryptorImpl.class, cryptor.fileNameCryptor(-1540072521));
+ }
+ }
+
+ @Test
+ public void testExplicitDestruction() {
+ UVFMasterkey masterkey = Mockito.mock(UVFMasterkey.class);
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ cryptor.destroy();
+ Mockito.verify(masterkey).destroy();
+ Mockito.when(masterkey.isDestroyed()).thenReturn(true);
+ Assertions.assertTrue(cryptor.isDestroyed());
+ }
+ }
+
+ @Test
+ public void testImplicitDestruction() {
+ UVFMasterkey masterkey = Mockito.mock(UVFMasterkey.class);
+ try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) {
+ Assertions.assertFalse(cryptor.isDestroyed());
+ }
+ Mockito.verify(masterkey).destroy();
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/CryptorProviderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/CryptorProviderImplTest.java
new file mode 100644
index 0000000..be4bc9d
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/CryptorProviderImplTest.java
@@ -0,0 +1,23 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.RevolvingMasterkey;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.security.SecureRandom;
+
+public class CryptorProviderImplTest {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
+
+ @Test
+ public void testProvide() {
+ RevolvingMasterkey masterkey = Mockito.mock(RevolvingMasterkey.class);
+ CryptorProvider provider = new CryptorProviderImpl();
+ Assertions.assertInstanceOf(CryptorImpl.class, provider.provide(masterkey, RANDOM_MOCK));
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImplTest.java
new file mode 100644
index 0000000..fd8f6ed
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/DirectoryContentCryptorImplTest.java
@@ -0,0 +1,162 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.security.SecureRandom;
+
+class DirectoryContentCryptorImplTest {
+
+ private static final SecureRandom CSPRNG = new SecureRandom();
+ private static UVFMasterkey masterkey;
+ private static DirectoryContentCryptorImpl dirCryptor;
+
+ @BeforeAll
+ public static void setUp() {
+ // copied from UVFMasterkeyTest:
+ String json = "{\n" +
+ " \"fileFormat\": \"AES-256-GCM-32k\",\n" +
+ " \"nameFormat\": \"AES-SIV-512-B64URL\",\n" +
+ " \"seeds\": {\n" +
+ " \"HDm38g\": \"ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs\",\n" +
+ " \"gBryKw\": \"PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0\",\n" +
+ " \"QBsJFg\": \"Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y\"\n" +
+ " },\n" +
+ " \"initialSeed\": \"HDm38i\",\n" +
+ " \"latestSeed\": \"QBsJFo\",\n" +
+ " \"kdf\": \"HKDF-SHA512\",\n" +
+ " \"kdfSalt\": \"NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D-6oiIjr8\",\n" +
+ " \"org.example.customfield\": 42\n" +
+ "}";
+ masterkey = UVFMasterkey.fromDecryptedPayload(json);
+ dirCryptor = (DirectoryContentCryptorImpl) CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT).provide(masterkey, CSPRNG).directoryContentCryptor();
+ }
+
+ @Test
+ @DisplayName("encrypt and decrypt dir.uvf files")
+ public void encryptAndDecryptDirectoryMetadata() {
+ DirectoryMetadataImpl origMetadata = dirCryptor.newDirectoryMetadata();
+
+ byte[] encryptedMetadata = dirCryptor.encryptDirectoryMetadata(origMetadata);
+ DirectoryMetadataImpl decryptedMetadata = dirCryptor.decryptDirectoryMetadata(encryptedMetadata);
+
+ Assertions.assertEquals(origMetadata.seedId(), decryptedMetadata.seedId());
+ Assertions.assertArrayEquals(origMetadata.dirId(), decryptedMetadata.dirId());
+ }
+
+ @Test
+ @DisplayName("encrypt WELCOME.rtf in root dir")
+ public void testEncryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Encrypting enc = dirCryptor.fileNameEncryptor(rootDirMetadata);
+ String ciphertext = enc.encrypt("WELCOME.rtf");
+ Assertions.assertEquals("Dx1binBPsg_KNby6KFD_2k3vZHPgo39rg4ks.uvf", ciphertext);
+ }
+
+ @Test
+ @DisplayName("decrypt WELCOME.rtf in root dir")
+ public void testDecryptReadme() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ DirectoryContentCryptor.Decrypting dec = dirCryptor.fileNameDecryptor(rootDirMetadata);
+ String plaintext = dec.decrypt("Dx1binBPsg_KNby6KFD_2k3vZHPgo39rg4ks.uvf");
+ Assertions.assertEquals("WELCOME.rtf", plaintext);
+ }
+
+ @Test
+ @DisplayName("get root dir path")
+ public void testRootDirPath() {
+ DirectoryMetadata rootDirMetadata = dirCryptor.rootDirectoryMetadata();
+ String path = dirCryptor.dirPath(rootDirMetadata);
+ Assertions.assertEquals("d/RZ/K7ZH7KBXULNEKBMGX3CU42PGUIAIX4", path);
+ }
+
+ @Nested
+ @DisplayName("Given a specific dir.uvf file")
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithDirectoryMetadata {
+
+ DirectoryMetadataImpl dirUvf;
+ DirectoryContentCryptor.Encrypting enc;
+ DirectoryContentCryptor.Decrypting dec;
+
+ @BeforeAll
+ public void setup() {
+ dirUvf = new DirectoryMetadataImpl(masterkey.currentRevision(), new byte[32]);
+ enc = dirCryptor.fileNameEncryptor(dirUvf);
+ dec = dirCryptor.fileNameDecryptor(dirUvf);
+ }
+
+ @DisplayName("encrypt multiple file names")
+ @ParameterizedTest(name = "fileNameEncryptor.encrypt('{0}') == '{1}'")
+ @CsvSource({
+ "file1.txt, NIWamUJBS3u619f3yKOWlT2q_raURsHXhg==.uvf",
+ "file2.txt, _EWTVc9qooJQyk-P9pwQkvSu9mFb0UWNeg==.uvf",
+ "file3.txt, dunZsv8VRuh81R-u6pioPx2DWeQAU0nLfw==.uvf",
+ "file4.txt, 2-clI661p9TBSzC2IJjvBF3ehaKas5Vqxg==.uvf"
+ })
+ public void testBulkEncryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(ciphertext, enc.encrypt(plaintext));
+ }
+
+ @DisplayName("decrypt multiple file names")
+ @ParameterizedTest(name = "fileNameDecryptor.decrypt('{1}') == '{0}'")
+ @CsvSource({
+ "file1.txt, NIWamUJBS3u619f3yKOWlT2q_raURsHXhg==.uvf",
+ "file2.txt, _EWTVc9qooJQyk-P9pwQkvSu9mFb0UWNeg==.uvf",
+ "file3.txt, dunZsv8VRuh81R-u6pioPx2DWeQAU0nLfw==.uvf",
+ "file4.txt, 2-clI661p9TBSzC2IJjvBF3ehaKas5Vqxg==.uvf"
+ })
+ public void testBulkDecryption(String plaintext, String ciphertext) {
+ Assertions.assertEquals(plaintext, dec.decrypt(ciphertext));
+ }
+
+ @Test
+ @DisplayName("decrypt file with invalid extension")
+ public void testDecryptMalformed1() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ dec.decrypt("NIWamUJBS3u619f3yKOWlT2q_raURsHXhg==.INVALID");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with unauthentic ciphertext")
+ public void testDecryptMalformed2() {
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ dec.decrypt("INVALIDamUJBS3u619f3yKOWlT2q_raURsHXhg==.uvf");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with incorrect seed")
+ public void testDecryptMalformed3() {
+ DirectoryMetadataImpl differentRevision = new DirectoryMetadataImpl(masterkey.firstRevision(), new byte[32]);
+ DirectoryContentCryptor.Decrypting differentRevisionDec = dirCryptor.fileNameDecryptor(differentRevision);
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ differentRevisionDec.decrypt("NIWamUJBS3u619f3yKOWlT2q_raURsHXhg==.uvf");
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file with incorrect dirId")
+ public void testDecryptMalformed4() {
+ DirectoryMetadataImpl differentDirId = new DirectoryMetadataImpl(masterkey.firstRevision(), new byte[]{(byte) 0xDE, (byte) 0x0AD});
+ DirectoryContentCryptor.Decrypting differentDirIdDec = dirCryptor.fileNameDecryptor(differentDirId);
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ differentDirIdDec.decrypt("NIWamUJBS3u619f3yKOWlT2q_raURsHXhg==.uvf");
+ });
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplBenchmark.java
new file mode 100644
index 0000000..39a4f33
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplBenchmark.java
@@ -0,0 +1,65 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
+ */
+@State(Scope.Thread)
+@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
+@BenchmarkMode(value = {Mode.AverageTime})
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class FileContentCryptorImplBenchmark {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
+ private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES");
+ private final byte[] headerNonce = new byte[FileHeaderImpl.NONCE_LEN];
+ private final ByteBuffer cleartextChunk = ByteBuffer.allocate(Constants.PAYLOAD_SIZE);
+ private final ByteBuffer ciphertextChunk = ByteBuffer.allocate(Constants.CHUNK_SIZE);
+ private final FileContentCryptorImpl fileContentCryptor = new FileContentCryptorImpl(RANDOM_MOCK);
+ private long chunkNumber;
+
+ @Setup(Level.Trial)
+ public void prepareData() {
+ cleartextChunk.rewind();
+ fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, 0l, new byte[12], ENC_KEY);
+ ciphertextChunk.flip();
+ }
+
+ @Setup(Level.Invocation)
+ public void shuffleData() {
+ chunkNumber = RANDOM_MOCK.nextLong();
+ cleartextChunk.rewind();
+ ciphertextChunk.rewind();
+ RANDOM_MOCK.nextBytes(headerNonce);
+ RANDOM_MOCK.nextBytes(cleartextChunk.array());
+ }
+
+ @Benchmark
+ public void benchmarkEncryption() {
+ fileContentCryptor.encryptChunk(cleartextChunk, ciphertextChunk, chunkNumber, headerNonce, ENC_KEY);
+ }
+
+ @Benchmark
+ public void benchmarkDecryption() throws AuthenticationFailedException {
+ fileContentCryptor.decryptChunk(ciphertextChunk, cleartextChunk, 0l, new byte[12], ENC_KEY);
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplTest.java
new file mode 100644
index 0000000..838b0ee
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileContentCryptorImplTest.java
@@ -0,0 +1,307 @@
+package org.cryptomator.cryptolib.v3;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.CipherSupplier;
+import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
+import org.cryptomator.cryptolib.common.GcmTestHelper;
+import org.cryptomator.cryptolib.common.ObjectPool;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.cryptomator.cryptolib.common.SeekableByteChannelMock;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
+
+import javax.crypto.Cipher;
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.cryptomator.cryptolib.v3.Constants.GCM_NONCE_SIZE;
+import static org.cryptomator.cryptolib.v3.Constants.GCM_TAG_SIZE;
+
+public class FileContentCryptorImplTest {
+
+ // AES-GCM implementation requires non-repeating nonces, still we need deterministic nonces for testing
+ private static final SecureRandom CSPRNG = Mockito.spy(SecureRandomMock.cycle((byte) 0xF0, (byte) 0x0F));
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+
+ private FileHeaderImpl header;
+ private FileHeaderCryptorImpl headerCryptor;
+ private FileContentCryptorImpl fileContentCryptor;
+ private Cryptor cryptor;
+
+ @BeforeEach
+ public void setup() {
+ header = new FileHeaderImpl(-1540072521, new byte[FileHeaderImpl.NONCE_LEN], new DestroyableSecretKey(new byte[FileHeaderImpl.CONTENT_KEY_LEN], "AES"));
+ headerCryptor = new FileHeaderCryptorImpl(MASTERKEY, CSPRNG, -1540072521);
+ fileContentCryptor = new FileContentCryptorImpl(CSPRNG);
+ cryptor = Mockito.mock(Cryptor.class);
+ Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor);
+ Mockito.when(cryptor.fileHeaderCryptor()).thenReturn(headerCryptor);
+
+ // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
+ GcmTestHelper.reset((mode, key, params) -> {
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
+ cipher.get();
+ }
+ });
+ }
+
+ @Test
+ public void testDecryptedEncryptedEqualsPlaintext() throws AuthenticationFailedException {
+ DestroyableSecretKey fileKey = new DestroyableSecretKey(new byte[16], "AES");
+ ByteBuffer ciphertext = ByteBuffer.allocate(fileContentCryptor.ciphertextChunkSize());
+ ByteBuffer cleartext = ByteBuffer.allocate(fileContentCryptor.cleartextChunkSize());
+ fileContentCryptor.encryptChunk(UTF_8.encode("asd"), ciphertext, 42l, new byte[12], fileKey);
+ ciphertext.flip();
+ fileContentCryptor.decryptChunk(ciphertext, cleartext, 42l, new byte[12], fileKey);
+ cleartext.flip();
+ Assertions.assertEquals(UTF_8.encode("asd"), cleartext);
+ }
+
+ @Nested
+ public class Encryption {
+
+ @BeforeEach
+ public void resetGcmNonce() {
+ // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
+ GcmTestHelper.reset((mode, key, params) -> {
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
+ cipher.get();
+ }
+ });
+ }
+
+ @DisplayName("encrypt chunk with invalid size")
+ @ParameterizedTest(name = "cleartext size: {0}")
+ @ValueSource(ints = {Constants.PAYLOAD_SIZE + 1})
+ public void testEncryptChunkOfInvalidSize(int size) {
+ ByteBuffer cleartext = ByteBuffer.allocate(size);
+
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ fileContentCryptor.encryptChunk(cleartext, 0, header);
+ });
+ }
+
+ @Test
+ @DisplayName("encrypt chunk")
+ public void testChunkEncryption() {
+ Mockito.doAnswer(invocation -> {
+ byte[] nonce = invocation.getArgument(0);
+ Arrays.fill(nonce, (byte) 0x33);
+ return null;
+ }).when(CSPRNG).nextBytes(Mockito.any());
+ ByteBuffer cleartext = StandardCharsets.US_ASCII.encode(CharBuffer.wrap("hello world"));
+ ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, header);
+ // echo -n "hello world" | openssl enc -aes-256-gcm -K 0 -iv 333333333333333333333333 -a
+ byte[] expected = BaseEncoding.base64().decode("MzMzMzMzMzMzMzMzbYvL7CusRmzk70Kn1QxFA5WQg/hgKeba4bln");
+ Assertions.assertEquals(ByteBuffer.wrap(expected), ciphertext);
+ }
+
+ @Test
+ @DisplayName("encrypt chunk with offset ByteBuffer")
+ public void testChunkEncryptionWithByteBufferView() {
+ Mockito.doAnswer(invocation -> {
+ byte[] nonce = invocation.getArgument(0);
+ Arrays.fill(nonce, (byte) 0x33);
+ return null;
+ }).when(CSPRNG).nextBytes(Mockito.any());
+ ByteBuffer cleartext = US_ASCII.encode("12345hello world12345");
+ cleartext.position(5).limit(16);
+ ByteBuffer ciphertext = fileContentCryptor.encryptChunk(cleartext, 0, header);
+ byte[] expected = BaseEncoding.base64().decode("MzMzMzMzMzMzMzMzbYvL7CusRmzk70Kn1QxFA5WQg/hgKeba4bln");
+ Assertions.assertEquals(ByteBuffer.wrap(expected), ciphertext);
+ }
+
+ @Test
+ @DisplayName("encrypt chunk with too small ciphertext buffer")
+ public void testChunkEncryptionWithBufferUnderflow() {
+ ByteBuffer cleartext = StandardCharsets.US_ASCII.encode(CharBuffer.wrap("hello world"));
+ ByteBuffer ciphertext = ByteBuffer.allocate(Constants.CHUNK_SIZE - 1);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ fileContentCryptor.encryptChunk(cleartext, ciphertext, 0, header);
+ });
+ }
+
+ @Test
+ @DisplayName("encrypt file")
+ public void testFileEncryption() throws IOException {
+ Mockito.doAnswer(invocation -> {
+ byte[] nonce = invocation.getArgument(0);
+ Arrays.fill(nonce, (byte) 0x55); // header nonce
+ return null;
+ }).doAnswer(invocation -> {
+ byte[] nonce = invocation.getArgument(0);
+ Arrays.fill(nonce, (byte) 0x77); // header content key
+ return null;
+ }).doAnswer(invocation -> {
+ byte[] nonce = invocation.getArgument(0);
+ Arrays.fill(nonce, (byte) 0xAA); // chunk nonce
+ return null;
+ }).when(CSPRNG).nextBytes(Mockito.any());
+ ByteBuffer dst = ByteBuffer.allocate(200);
+ SeekableByteChannel dstCh = new SeekableByteChannelMock(dst);
+ try (WritableByteChannel ch = new EncryptingWritableByteChannel(dstCh, cryptor)) {
+ ch.write(StandardCharsets.US_ASCII.encode("hello world"));
+ }
+ dst.flip();
+ byte[] ciphertext = new byte[dst.remaining()];
+ dst.get(ciphertext);
+ byte[] expected = BaseEncoding.base64().decode("dXZmAKQ0W7dVVVVVVVVVVVVVVVUcowd1FbpM8eMdABtmD/I3RO0n0rV2V3iDYXb++QHGHkO9MqYKLnd7ZjeoyNpG1Nmqqqqqqqqqqqqqqqq3J+X9CFc6SL5hl+6GdwnBgUYN6cPnoxF4C2Q=");
+ Assertions.assertArrayEquals(expected, ciphertext);
+ }
+
+ }
+
+ @Nested
+ public class Decryption {
+
+ @DisplayName("decrypt chunk with invalid size")
+ @ParameterizedTest(name = "ciphertext size: {0}")
+ @ValueSource(ints = {0, Constants.GCM_NONCE_SIZE + Constants.GCM_TAG_SIZE - 1, Constants.CHUNK_SIZE + 1})
+ public void testDecryptChunkOfInvalidSize(int size) {
+ ByteBuffer ciphertext = ByteBuffer.allocate(size);
+
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt chunk")
+ public void testChunkDecryption() throws AuthenticationFailedException {
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
+ ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
+ ByteBuffer expected = StandardCharsets.US_ASCII.encode("hello world");
+ Assertions.assertEquals(expected, cleartext);
+ }
+
+ @Test
+ @DisplayName("decrypt chunk with offset ByteBuffer")
+ public void testChunkDecryptionWithByteBufferView() throws AuthenticationFailedException {
+ byte[] actualCiphertext = BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv");
+ ByteBuffer ciphertext = ByteBuffer.allocate(100);
+ ciphertext.position(10);
+ ciphertext.put(actualCiphertext);
+ ciphertext.position(10).limit(10 + actualCiphertext.length);
+ ByteBuffer cleartext = fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
+ ByteBuffer expected = US_ASCII.encode("hello world");
+ Assertions.assertEquals(expected, cleartext);
+ }
+
+ @Test
+ @DisplayName("decrypt chunk with too small cleartext buffer")
+ public void testChunkDecryptionWithBufferUnderflow() {
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv"));
+ ByteBuffer cleartext = ByteBuffer.allocate(Constants.PAYLOAD_SIZE - 1);
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ fileContentCryptor.decryptChunk(ciphertext, cleartext, 0, header, true);
+ });
+ }
+
+ @Test
+ @DisplayName("decrypt file")
+ public void testFileDecryption() throws IOException {
+ byte[] ciphertext = BaseEncoding.base64().decode("dXZmAKQ0W7dVVVVVVVVVVVVVVVUcowd1FbpM8eMdABtmD/I3RO0n0rV2V3iDYXb++QHGHkO9MqYKLnd7ZjeoyNpG1Nmqqqqqqqqqqqqqqqq3J+X9CFc6SL5hl+6GdwnBgUYN6cPnoxF4C2Q=");
+ ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+ ByteBuffer result = ByteBuffer.allocate(20);
+ try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+ int read = cleartextCh.read(result);
+ Assertions.assertEquals(11, read);
+ byte[] expected = "hello world".getBytes(StandardCharsets.US_ASCII);
+ Assertions.assertArrayEquals(expected, Arrays.copyOfRange(result.array(), 0, read));
+ }
+ }
+
+ @Test
+ @DisplayName("decrypt file with unauthentic file header")
+ public void testDecryptionWithTooShortHeader() throws InterruptedException, IOException {
+ byte[] ciphertext = BaseEncoding.base64().decode("AAAAAAAA");
+ ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+ try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+ Assertions.assertThrows(EOFException.class, () -> {
+ cleartextCh.read(ByteBuffer.allocate(3));
+ });
+ }
+ }
+
+ @DisplayName("decrypt unauthentic chunk")
+ @ParameterizedTest(name = "unauthentic {1}")
+ @CsvSource(value = {
+ "vVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv, NONCE",
+ "VVVVVVVVVVVVVVVVNHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHv, CONTENT",
+ "VVVVVVVVVVVVVVVVnHVdh+EbedvPeiCwCdaTYpzn1CXQjhSh7PHV, TAG",
+ })
+ public void testUnauthenticChunkDecryption(String chunkData, String ignored) {
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode(chunkData));
+
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ fileContentCryptor.decryptChunk(ciphertext, 0, header, true);
+ });
+ }
+
+ @DisplayName("decrypt unauthentic file")
+ @ParameterizedTest(name = "unauthentic {1} in first chunk")
+ @CsvSource(value = {
+ "dXZmAKQ0W7dVVVVVVVVVVVVVVVUcowd1FbpM8eMdABtmD/I3RO0n0rV2V3iDYXb++QHGHvqv753T4D5ZzPJtHYv0+ieqqqqxqqqqqqqqqqq3J+X9CFc6SL5hl+6GdwnBgUYN6cPnoxF4C2Q=, NONCE",
+ "dXZmAKQ0W7dVVVVVVVVVVVVVVVUcowd1FbpM8eMdABtmD/I3RO0n0rV2V3iDYXb++QHGHvqv753T4D5ZzPJtHYv0+ieqqqqqqqqqqqqqqqq3JxX9CFc6SL5hl+6GdwnBgUYN6cPnoxF4C2Q=, CONTENT",
+ "dXZmAKQ0W7dVVVVVVVVVVVVVVVUcowd1FbpM8eMdABtmD/I3RO0n0rV2V3iDYXb++QHGHvqv753T4D5ZzPJtHYv0+ieqqqqqqqqqqqqqqqq3J+X9CFc6SL5hl+6GdwnBgUYN6cPnoxF4C2x=, TAG",
+ })
+ public void testDecryptionWithUnauthenticFirstChunk(String fileData, String ignored) throws IOException {
+ byte[] ciphertext = BaseEncoding.base64().decode(fileData);
+
+ ReadableByteChannel ciphertextCh = Channels.newChannel(new ByteArrayInputStream(ciphertext));
+
+ try (ReadableByteChannel cleartextCh = new DecryptingReadableByteChannel(ciphertextCh, cryptor, true)) {
+ IOException thrown = Assertions.assertThrows(IOException.class, () -> {
+ cleartextCh.read(ByteBuffer.allocate(3));
+ });
+ MatcherAssert.assertThat(thrown.getCause(), CoreMatchers.instanceOf(AuthenticationFailedException.class));
+ }
+ }
+
+ @Test
+ @DisplayName("decrypt chunk with unauthentic tag but skipping authentication")
+ public void testChunkDecryptionWithUnauthenticTagSkipAuth() {
+ ByteBuffer dummyCiphertext = ByteBuffer.allocate(GCM_NONCE_SIZE + GCM_TAG_SIZE);
+ FileHeader header = Mockito.mock(FileHeader.class);
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> {
+ fileContentCryptor.decryptChunk(dummyCiphertext, 0, header, false);
+ });
+ }
+
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileContentEncryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v3/FileContentEncryptorBenchmark.java
new file mode 100644
index 0000000..44df3e6
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileContentEncryptorBenchmark.java
@@ -0,0 +1,128 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
+ */
+@State(Scope.Thread)
+@Warmup(iterations = 2)
+@Measurement(iterations = 2)
+@BenchmarkMode(value = {Mode.SingleShotTime})
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+public class FileContentEncryptorBenchmark {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+
+ private CryptorImpl cryptor;
+
+ @Setup(Level.Iteration)
+ public void shuffleData() {
+ cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK);
+ }
+
+ @Benchmark
+ public void benchmark100MegabytesEncryption() throws IOException {
+ ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
+ try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
+ for (int i = 0; i < 100; i++) {
+ ch.write(megabyte);
+ megabyte.clear();
+ }
+ }
+ }
+
+ @Benchmark
+ public void benchmark10MegabytesEncryption() throws IOException {
+ ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
+ try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
+ for (int i = 0; i < 10; i++) {
+ ch.write(megabyte);
+ megabyte.clear();
+ }
+ }
+ }
+
+ @Benchmark
+ public void benchmark1MegabytesEncryption() throws IOException {
+ ByteBuffer megabyte = ByteBuffer.allocate(1 * 1024 * 1024);
+ try (WritableByteChannel ch = new EncryptingWritableByteChannel(new NullSeekableByteChannel(), cryptor)) {
+ ch.write(megabyte);
+ megabyte.clear();
+ }
+ }
+
+ private static class NullSeekableByteChannel implements SeekableByteChannel {
+
+ boolean open;
+
+ @Override
+ public boolean isOpen() {
+ return open;
+ }
+
+ @Override
+ public void close() {
+ open = false;
+ }
+
+ @Override
+ public int read(ByteBuffer dst) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int write(ByteBuffer src) {
+ int delta = src.remaining();
+ src.position(src.position() + delta);
+ return delta;
+ }
+
+ @Override
+ public long position() {
+ return 0;
+ }
+
+ @Override
+ public SeekableByteChannel position(long newPosition) {
+ return this;
+ }
+
+ @Override
+ public long size() {
+ return 0;
+ }
+
+ @Override
+ public SeekableByteChannel truncate(long size) {
+ return this;
+ }
+
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorBenchmark.java
new file mode 100644
index 0000000..b5e077b
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorBenchmark.java
@@ -0,0 +1,64 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Needs to be compiled via maven as the JMH annotation processor needs to do stuff...
+ */
+@State(Scope.Thread)
+@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
+@BenchmarkMode(value = {Mode.AverageTime})
+@OutputTimeUnit(TimeUnit.MICROSECONDS)
+public class FileHeaderCryptorBenchmark {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM;
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+ private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK, -1540072521);
+
+ private ByteBuffer validHeaderCiphertextBuf;
+ private FileHeader header;
+
+ @Setup(Level.Iteration)
+ public void prepareData() {
+ validHeaderCiphertextBuf = HEADER_CRYPTOR.encryptHeader(HEADER_CRYPTOR.create());
+ }
+
+ @Setup(Level.Invocation)
+ public void shuffleData() {
+ header = HEADER_CRYPTOR.create();
+ }
+
+ @Benchmark
+ public void benchmarkEncryption() {
+ HEADER_CRYPTOR.encryptHeader(header);
+ }
+
+ @Benchmark
+ public void benchmarkDecryption() throws AuthenticationFailedException {
+ HEADER_CRYPTOR.decryptHeader(validHeaderCiphertextBuf);
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImplTest.java
new file mode 100644
index 0000000..58d97e7
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderCryptorImplTest.java
@@ -0,0 +1,101 @@
+package org.cryptomator.cryptolib.v3;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.FileHeader;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.CipherSupplier;
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.cryptomator.cryptolib.common.GcmTestHelper;
+import org.cryptomator.cryptolib.common.ObjectPool;
+import org.cryptomator.cryptolib.common.SecureRandomMock;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.crypto.Cipher;
+import java.nio.ByteBuffer;
+import java.security.SecureRandom;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+
+public class FileHeaderCryptorImplTest {
+
+ private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM;
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+
+ private FileHeaderCryptorImpl headerCryptor;
+
+ @BeforeEach
+ public void setup() {
+ headerCryptor = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK, -1540072521);
+
+ // reset cipher state to avoid InvalidAlgorithmParameterExceptions due to IV-reuse
+ GcmTestHelper.reset((mode, key, params) -> {
+ try (ObjectPool.Lease cipher = CipherSupplier.AES_GCM.encryptionCipher(key, params)) {
+ cipher.get();
+ }
+ });
+ }
+
+ @Test
+ public void testHeaderSize() {
+ Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.headerSize());
+ Assertions.assertEquals(FileHeaderImpl.SIZE, headerCryptor.encryptHeader(headerCryptor.create()).limit());
+ }
+
+ @Test
+ public void testSubkeyGeneration() {
+ DestroyableSecretKey subkey = MASTERKEY.subKey(-1540072521, 32, "fileHeader".getBytes(), "AES");
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("PwnW2t_pK9dmzc-GTLdBSaB8ilcwsTq4sYOeiyo3cpU"), subkey.getEncoded());
+ }
+
+ @Test
+ public void testEncryption() {
+ DestroyableSecretKey contentKey = new DestroyableSecretKey(new byte[FileHeaderImpl.CONTENT_KEY_LEN], "AES");
+ FileHeader header = new FileHeaderImpl(-1540072521, new byte[FileHeaderImpl.NONCE_LEN], contentKey);
+
+ ByteBuffer ciphertext = headerCryptor.encryptHeader(header);
+
+ Assertions.assertArrayEquals(Base64.getDecoder().decode("dXZmAKQ0W7cAAAAAAAAAAAAAAAA/UGgFA8QGho7E1QTsHWyZIVFqabbGJ/TCwvp3StG0JTkKGj3hwERhnFmZek61Xtc="), ciphertext.array());
+ }
+
+ @Test
+ public void testDecryption() throws AuthenticationFailedException {
+ byte[] ciphertext = BaseEncoding.base64().decode("dXZmAKQ0W7cAAAAAAAAAAAAAAAA/UGgFA8QGho7E1QTsHWyZIVFqabbGJ/TCwvp3StG0JTkKGj3hwERhnFmZek61Xtc=");
+ FileHeaderImpl header = headerCryptor.decryptHeader(ByteBuffer.wrap(ciphertext));
+ Assertions.assertArrayEquals(new byte[FileHeaderImpl.NONCE_LEN], header.getNonce());
+ Assertions.assertArrayEquals(new byte[FileHeaderImpl.CONTENT_KEY_LEN], header.getContentKey().getEncoded());
+ }
+
+ @Test
+ public void testDecryptionWithTooShortHeader() {
+ ByteBuffer ciphertext = ByteBuffer.allocate(7);
+
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ headerCryptor.decryptHeader(ciphertext);
+ });
+ }
+
+ @Test
+ public void testDecryptionWithInvalidTag() {
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("dXZmAKQ0W7cAAAAAAAAAAAAAAAA/UGgFA8QGho7E1QTsHWyZIVFqabbGJ/TCwvp3StG0JTkKGj3hwERhnFmZek61XtX="));
+
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ headerCryptor.decryptHeader(ciphertext);
+ });
+ }
+
+ @Test
+ public void testDecryptionWithInvalidCiphertext() {
+ ByteBuffer ciphertext = ByteBuffer.wrap(BaseEncoding.base64().decode("dXZmAKQ0W7cAAAAAAAAAAAAAAAA/UGgFA8QGho7E1QTsHWyZIVFqabbGJ/XCwvp3StG0JTkKGj3hwERhnFmZek61Xtc="));
+
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ headerCryptor.decryptHeader(ciphertext);
+ });
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderImplTest.java
new file mode 100644
index 0000000..85f46af
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileHeaderImplTest.java
@@ -0,0 +1,31 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.common.DestroyableSecretKey;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+public class FileHeaderImplTest {
+
+ @Test
+ public void testConstructionFailsWithInvalidNonceSize() {
+ DestroyableSecretKey contentKey = new DestroyableSecretKey(new byte[FileHeaderImpl.CONTENT_KEY_LEN], "AES");
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ new FileHeaderImpl(-1540072521, new byte[3], contentKey);
+ });
+ }
+
+ @Test
+ public void testDestruction() {
+ byte[] nonNullKey = new byte[FileHeaderImpl.CONTENT_KEY_LEN];
+ Arrays.fill(nonNullKey, (byte) 0x42);
+ DestroyableSecretKey contentKey = new DestroyableSecretKey(nonNullKey, "AES");
+ FileHeaderImpl header = new FileHeaderImpl(-1540072521, new byte[FileHeaderImpl.NONCE_LEN], contentKey);
+ Assertions.assertFalse(header.isDestroyed());
+ header.destroy();
+ Assertions.assertTrue(header.isDestroyed());
+ Assertions.assertTrue(contentKey.isDestroyed());
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java
new file mode 100644
index 0000000..fae565c
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java
@@ -0,0 +1,143 @@
+package org.cryptomator.cryptolib.v3;
+
+import com.google.common.io.BaseEncoding;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
+import org.cryptomator.cryptolib.api.PerpetualMasterkey;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.HKDFHelper;
+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.util.Base64;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+
+public class FileNameCryptorImplTest {
+
+ private static final BaseEncoding BASE32 = BaseEncoding.base32();
+ private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getUrlDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU"));
+ private static final byte[] KDF_SALT = Base64.getUrlDecoder().decode("HE4OP-2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY");
+ private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521);
+
+ private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(MASTERKEY, -1540072521);
+
+ private static Stream 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(BASE32, origName);
+ String encrypted2 = filenameCryptor.encryptFilename(BASE32, origName);
+ String decrypted = filenameCryptor.decryptFilename(BASE32, 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
+ String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii
+ String encryptedPath3a = filenameCryptor.encryptFilename(BASE32, originalPath3);
+ String encryptedPath3b = filenameCryptor.encryptFilename(BASE32, originalPath3);
+ String decryptedPath3 = filenameCryptor.decryptFilename(BASE32, encryptedPath3a);
+
+ Assertions.assertEquals(encryptedPath3a, encryptedPath3b);
+ Assertions.assertEquals(originalPath3, decryptedPath3);
+ }
+
+ @DisplayName("hash root dir id")
+ @Test
+ public void testHashRootDirId() {
+ final byte[] rootDirId = Base64.getDecoder().decode("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc=");
+ final String hashedRootDirId = filenameCryptor.hashDirectoryId(rootDirId);
+ Assertions.assertEquals("6DYU3E5BTPAZ4DWEQPQK3AIHX2DXSPHG", hashedRootDirId);
+ }
+
+ @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.getBytes(UTF_8));
+ final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId.getBytes(UTF_8));
+ Assertions.assertEquals(hashedDirectory1, hashedDirectory2);
+ }
+
+ @Test
+ @DisplayName("decrypt non-ciphertext")
+ public void testDecryptionOfMalformedFilename() {
+ AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ filenameCryptor.decryptFilename(BASE32, "lol");
+ });
+ MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class));
+ }
+
+ @Test
+ @DisplayName("decrypt tampered ciphertext")
+ public void testDecryptionOfManipulatedFilename() {
+ final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8);
+ encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte
+ String ciphertextName = new String(encrypted, UTF_8);
+
+ AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ filenameCryptor.decryptFilename(BASE32, ciphertextName);
+ });
+ MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class));
+ }
+
+ @Test
+ @DisplayName("encrypt with different AD")
+ public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() {
+ final String encrypted1 = filenameCryptor.encryptFilename(BASE32, "test", "ad1".getBytes(UTF_8));
+ final String encrypted2 = filenameCryptor.encryptFilename(BASE32, "test", "ad2".getBytes(UTF_8));
+ Assertions.assertNotEquals(encrypted1, encrypted2);
+ }
+
+ @Test
+ @DisplayName("decrypt ciphertext with correct AD")
+ public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException {
+ final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "ad".getBytes(UTF_8));
+ final String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted, "ad".getBytes(UTF_8));
+ Assertions.assertEquals("test", decrypted);
+ }
+
+ @Test
+ @DisplayName("decrypt ciphertext with incorrect AD")
+ public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() {
+ final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8));
+ final byte[] ad = "wrong".getBytes(UTF_8);
+
+ Assertions.assertThrows(AuthenticationFailedException.class, () -> {
+ filenameCryptor.decryptFilename(BASE32, encrypted, ad);
+ });
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptolib/v3/UVFIntegrationTest.java b/src/test/java/org/cryptomator/cryptolib/v3/UVFIntegrationTest.java
new file mode 100644
index 0000000..346920c
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptolib/v3/UVFIntegrationTest.java
@@ -0,0 +1,171 @@
+package org.cryptomator.cryptolib.v3;
+
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.DirectoryContentCryptor;
+import org.cryptomator.cryptolib.api.DirectoryMetadata;
+import org.cryptomator.cryptolib.api.UVFMasterkey;
+import org.cryptomator.cryptolib.common.DecryptingReadableByteChannel;
+import org.cryptomator.cryptolib.common.EncryptingWritableByteChannel;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.Channels;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Base64;
+
+public class UVFIntegrationTest {
+
+ private static final SecureRandom CSPRNG = new SecureRandom();
+ private static UVFMasterkey masterkey;
+ private static Cryptor cryptor;
+
+ @BeforeAll
+ public static void setUp() {
+ // copied from UVFMasterkeyTest:
+ String json = "{\n" +
+ " \"fileFormat\": \"AES-256-GCM-32k\",\n" +
+ " \"nameFormat\": \"AES-SIV-512-B64URL\",\n" +
+ " \"seeds\": {\n" +
+ " \"HDm38g\": \"ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs\",\n" +
+ " \"gBryKw\": \"PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0\",\n" +
+ " \"QBsJFg\": \"Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y\"\n" +
+ " },\n" +
+ " \"initialSeed\": \"HDm38i\",\n" +
+ " \"latestSeed\": \"QBsJFo\",\n" +
+ " \"kdf\": \"HKDF-SHA512\",\n" +
+ " \"kdfSalt\": \"NIlr89R7FhochyP4yuXZmDqCnQ0dBB3UZ2D-6oiIjr8\",\n" +
+ " \"org.example.customfield\": 42\n" +
+ "}";
+ masterkey = UVFMasterkey.fromDecryptedPayload(json);
+ cryptor = CryptorProvider.forScheme(CryptorProvider.Scheme.UVF_DRAFT).provide(masterkey, CSPRNG);
+ }
+
+ @Test
+ @DisplayName("root dir id must be deterministic")
+ public void testRootDirId() {
+ byte[] rootDirId = masterkey.rootDirId();
+ Assertions.assertEquals("5WEGzwKkAHPwVSjT2Brr3P3zLz7oMiNpMn/qBvht7eM=", Base64.getEncoder().encodeToString(rootDirId));
+ }
+
+ @Test
+ @DisplayName("root dir hash must be deterministic")
+ public void testRootDirHash() {
+ byte[] rootDirId = Base64.getDecoder().decode("5WEGzwKkAHPwVSjT2Brr3P3zLz7oMiNpMn/qBvht7eM=");
+ String dirHash = cryptor.fileNameCryptor(masterkey.firstRevision()).hashDirectoryId(rootDirId);
+ Assertions.assertEquals("RZK7ZH7KBXULNEKBMGX3CU42PGUIAIX4", dirHash);
+ }
+
+ @Test
+ @DisplayName("encrypt dir.uvf for root directory")
+ public void testRootDirUvfEncryption() {
+ DirectoryMetadata rootDirMetadata = cryptor.directoryContentCryptor().rootDirectoryMetadata();
+ byte[] result = cryptor.directoryContentCryptor().encryptDirectoryMetadata(rootDirMetadata);
+ Assertions.assertArrayEquals(new byte[]{0x75, 0x76, 0x66, 0x00}, Arrays.copyOf(result, 4), "expected to begin with UVF0 magic bytes");
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("HDm38i"), Arrays.copyOfRange(result, 4, 8), "expected seed to be initial seed");
+ }
+
+ @Test
+ @DisplayName("decrypt dir.uvf for root directory")
+ public void testRootDirUvfDecryption() {
+ byte[] input = Base64.getDecoder().decode("dXZmABw5t/Ievp74RjIgGHn4+/Zt32dmqmYhmHiPNQ5Q2z+WYb4z8NbnynTgMWlGBCc65bTqSt4Pqhj9EGhrn8KVbQqzBVWcZkLVr4tntfvgZoVJYkeD5w9mJMwRlQJwqiC0uR+Lk2aBT2cfdPT92e/6+t7nlvoYtoahMtowCqY=");
+ DirectoryMetadata result = cryptor.directoryContentCryptor().decryptDirectoryMetadata(input);
+ DirectoryMetadataImpl metadata = Assertions.assertInstanceOf(DirectoryMetadataImpl.class, result);
+ Assertions.assertArrayEquals(masterkey.rootDirId(), metadata.dirId());
+ Assertions.assertEquals(masterkey.firstRevision(), metadata.seedId());
+
+ }
+
+ @Test
+ @DisplayName("encrypt file containing 'Hello, World!'")
+ public void testContentEncryption() throws IOException {
+ byte[] result = encryptFile(StandardCharsets.UTF_8.encode("Hello, World!"), cryptor);
+ Assertions.assertArrayEquals(new byte[]{0x75, 0x76, 0x66, 0x00}, Arrays.copyOf(result, 4), "expected to begin with UVF0 magic bytes");
+ Assertions.assertArrayEquals(Base64.getUrlDecoder().decode("QBsJFo"), Arrays.copyOfRange(result, 4, 8), "expected seed to be latest seed");
+ }
+
+ @Test
+ @DisplayName("decrypt file containing 'Hello, World!'")
+ public void testContentDecryption() throws IOException {
+ byte[] input = Base64.getDecoder().decode("dXZmAEAbCRZxhI5sPsMiMlAQpwXzsOw13pBVX/yHydeHoOlHBS9d+wVpmRvzUKx5HQUmtGR4avjDownMNOS4sBX8G0SVc5dIADKnGUOwgF20kkc/EpGzrrgkS3C9lZoRPPOj3dm2ONfy3UkT1Q==");
+ byte[] result = decryptFile(ByteBuffer.wrap(input), cryptor);
+ Assertions.assertEquals(13, result.length);
+ Assertions.assertEquals("Hello, World!", new String(result, StandardCharsets.UTF_8));
+ }
+
+ @Test
+ @DisplayName("create reference directory structure")
+ public void testCreateReferenceDirStructure(@TempDir Path vaultDir) throws IOException {
+ DirectoryContentCryptor dirContentCryptor = cryptor.directoryContentCryptor();
+
+ // ROOT
+ DirectoryMetadata rootDirMetadata = cryptor.directoryContentCryptor().rootDirectoryMetadata();
+ String rootDirPath = dirContentCryptor.dirPath(rootDirMetadata);
+ String rootDirUvfFilePath = rootDirPath + "/dir.uvf";
+ byte[] rootDirUvfFileContents = dirContentCryptor.encryptDirectoryMetadata(rootDirMetadata);
+ Files.createDirectories(vaultDir.resolve(rootDirPath));
+ Files.write(vaultDir.resolve(rootDirUvfFilePath), rootDirUvfFileContents);
+ DirectoryContentCryptor.Encrypting filesWithinRootDir = dirContentCryptor.fileNameEncryptor(rootDirMetadata);
+
+ // ROOT/foo.txt
+ String fooFileName = filesWithinRootDir.encrypt("foo.txt");
+ String fooFilePath = rootDirPath + "/" + fooFileName;
+ byte[] fooFileContents = encryptFile(StandardCharsets.UTF_8.encode("Hello Foo"), cryptor);
+ Files.write(vaultDir.resolve(fooFilePath), fooFileContents);
+
+ // ROOT/subdir
+ DirectoryMetadata subDirMetadata = dirContentCryptor.newDirectoryMetadata();
+ String subDirName = filesWithinRootDir.encrypt("subdir");
+ String subDirUvfFilePath1 = rootDirPath + "/" + subDirName + "/dir.uvf";
+ byte[] subDirUvfFileContents1 = dirContentCryptor.encryptDirectoryMetadata(subDirMetadata);
+ Files.createDirectories(vaultDir.resolve(rootDirPath + "/" + subDirName));
+ Files.write(vaultDir.resolve(subDirUvfFilePath1), subDirUvfFileContents1);
+ String subDirPath = dirContentCryptor.dirPath(subDirMetadata);
+ String subDirUvfFilePath2 = subDirPath + "/dir.uvf";
+ byte[] subDirUvfFileContents2 = dirContentCryptor.encryptDirectoryMetadata(subDirMetadata);
+ Files.createDirectories(vaultDir.resolve(subDirPath));
+ Files.write(vaultDir.resolve(subDirUvfFilePath2), subDirUvfFileContents2);
+ DirectoryContentCryptor.Encrypting filesWithinSubDir = dirContentCryptor.fileNameEncryptor(subDirMetadata);
+
+ // ROOT/subdir/bar.txt
+ String barFileName = filesWithinSubDir.encrypt("bar.txt");
+ String barFilePath = subDirPath + "/" + barFileName;
+ byte[] barFileContents = encryptFile(StandardCharsets.UTF_8.encode("Hello Bar"), cryptor);
+ Files.write(vaultDir.resolve(barFilePath), barFileContents);
+
+ // set breakpoint here to inspect the created directory structure
+ System.out.println(vaultDir);
+
+ }
+
+ private static byte[] encryptFile(ByteBuffer cleartext, Cryptor cryptor) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (EncryptingWritableByteChannel ch = new EncryptingWritableByteChannel(Channels.newChannel(baos), cryptor)) {
+ ch.write(cleartext);
+ }
+ return baos.toByteArray();
+ }
+
+ private static byte[] decryptFile(ByteBuffer ciphertext, Cryptor cryptor) throws IOException {
+ assert ciphertext.hasArray();
+ byte[] in = ciphertext.array();
+ ByteBuffer result = ByteBuffer.allocate((int) cryptor.fileContentCryptor().cleartextSize(in.length) - cryptor.fileHeaderCryptor().headerSize());
+ try (DecryptingReadableByteChannel ch = new DecryptingReadableByteChannel(Channels.newChannel(new ByteArrayInputStream(in)), cryptor, true)) {
+ int read = ch.read(result);
+ Assertions.assertEquals(13, read);
+ }
+ return result.array();
+ }
+
+}