diff --git a/main/crypto-layer/pom.xml b/main/crypto-layer/pom.xml
index 5fe215908..032583900 100644
--- a/main/crypto-layer/pom.xml
+++ b/main/crypto-layer/pom.xml
@@ -50,6 +50,12 @@
commons-codec
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
org.cryptomator
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/CryptoException.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/CryptoException.java
index 42054592c..a1edd5565 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/CryptoException.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/CryptoException.java
@@ -1,3 +1,11 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
package org.cryptomator.crypto.engine;
import java.io.IOException;
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/Cryptor.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/Cryptor.java
index c1cc321e3..d55672f24 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/Cryptor.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/Cryptor.java
@@ -17,4 +17,10 @@ public interface Cryptor extends Destroyable {
FilenameCryptor getFilenameCryptor();
+ void randomizeMasterkey();
+
+ boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase);
+
+ byte[] writeKeysToMasterkeyFile(CharSequence passphrase);
+
}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/AesKeyWrap.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/AesKeyWrap.java
new file mode 100644
index 000000000..de5521223
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/AesKeyWrap.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.crypto.engine.impl;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+final class AesKeyWrap {
+
+ private static final String RFC3394_CIPHER = "AESWrap";
+
+ private AesKeyWrap() {
+ }
+
+ /**
+ * @param kek Key encrypting key
+ * @param key Key to be wrapped
+ * @return Wrapped key
+ */
+ public static byte[] wrap(SecretKey kek, SecretKey key) {
+ final Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(RFC3394_CIPHER);
+ cipher.init(Cipher.WRAP_MODE, kek);
+ } catch (InvalidKeyException e) {
+ throw new IllegalArgumentException("Invalid key.", e);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalStateException("Algorithm/Padding should exist.", e);
+ }
+
+ try {
+ return cipher.wrap(key);
+ } catch (InvalidKeyException | IllegalBlockSizeException e) {
+ throw new IllegalStateException("Unable to wrap key.", e);
+ }
+ }
+
+ /**
+ * @param kek Key encrypting key
+ * @param wrappedKey Key to be unwrapped
+ * @param keyAlgorithm Key designation, i.e. algorithm name to be associated with the unwrapped key.
+ * @return Unwrapped key
+ * @throws NoSuchAlgorithmException If keyAlgorithm is unknown
+ * @throws InvalidKeyException If unwrapping failed (i.e. wrong kek)
+ */
+ public static SecretKey unwrap(SecretKey kek, byte[] wrappedKey, String keyAlgorithm) throws InvalidKeyException, NoSuchAlgorithmException {
+ final Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(RFC3394_CIPHER);
+ cipher.init(Cipher.UNWRAP_MODE, kek);
+ } catch (InvalidKeyException ex) {
+ throw new IllegalArgumentException("Invalid key.", ex);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
+ throw new IllegalStateException("Algorithm/Padding should exist.", ex);
+ }
+
+ return (SecretKey) cipher.unwrap(wrappedKey, keyAlgorithm, Cipher.SECRET_KEY);
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
index f6dfd94b0..fb62c6cf2 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
@@ -1,26 +1,140 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
package org.cryptomator.crypto.engine.impl;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.FilenameCryptor;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
public class CryptorImpl implements Cryptor {
- private final SecretKey encryptionKey;
- private final SecretKey macKey;
- private final FilenameCryptor filenameCryptor;
+ private static final int SCRYPT_SALT_LENGTH = 8;
+ private static final int SCRYPT_COST_PARAM = 1 << 14;
+ private static final int SCRYPT_BLOCK_SIZE = 8;
+ private static final int KEYLENGTH_IN_BYTES = 32;
+ private static final String ENCRYPTION_ALG = "AES";
+ private static final String MAC_ALG = "HmacSHA256";
- public CryptorImpl(SecretKey encryptionKey, SecretKey macKey) {
- this.encryptionKey = encryptionKey;
- this.macKey = macKey;
- this.filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
+ private SecretKey encryptionKey;
+ private SecretKey macKey;
+ private final AtomicReference filenameCryptor = new AtomicReference<>();
+ private final SecureRandom randomSource;
+
+ public CryptorImpl(SecureRandom randomSource) {
+ this.randomSource = randomSource;
}
@Override
public FilenameCryptor getFilenameCryptor() {
- return filenameCryptor;
+ // lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509
+ FilenameCryptor cryptor = filenameCryptor.get();
+ if (cryptor == null) {
+ cryptor = new FilenameCryptorImpl(encryptionKey, macKey);
+ if (filenameCryptor.compareAndSet(null, cryptor)) {
+ return cryptor;
+ } else {
+ // CAS failed: other thread set an object
+ return filenameCryptor.get();
+ }
+ } else {
+ return cryptor;
+ }
+ }
+
+ @Override
+ public void randomizeMasterkey() {
+ final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
+ try {
+ randomSource.nextBytes(randomBytes);
+ encryptionKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
+ randomSource.nextBytes(randomBytes);
+ macKey = new SecretKeySpec(randomBytes, ENCRYPTION_ALG);
+ } finally {
+ Arrays.fill(randomBytes, (byte) 0x00);
+ }
+ }
+
+ @Override
+ public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
+ final KeyFile keyFile;
+ try {
+ final ObjectMapper om = new ObjectMapper();
+ keyFile = om.readValue(masterkeyFileContents, KeyFile.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Unable to parse masterkeyFileContents", e);
+ }
+
+ // check version
+ if (keyFile.getVersion() != KeyFile.CURRENT_VERSION) {
+ // TODO
+ // throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION);
+ throw new IllegalArgumentException("Unsupported key (expected version: " + KeyFile.CURRENT_VERSION + ", actual version: " + keyFile.getVersion() + ")");
+ }
+
+ final byte[] kekBytes = Scrypt.scrypt(passphrase, keyFile.getScryptSalt(), keyFile.getScryptCostParam(), keyFile.getScryptBlockSize(), KEYLENGTH_IN_BYTES);
+ try {
+ final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
+ this.encryptionKey = AesKeyWrap.unwrap(kek, keyFile.getEncryptionMasterKey(), ENCRYPTION_ALG);
+ this.macKey = AesKeyWrap.unwrap(kek, keyFile.getMacMasterKey(), MAC_ALG);
+ return true;
+ } catch (InvalidKeyException e) {
+ return false;
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Hard-coded algorithm doesn't exist.", e);
+ } finally {
+ Arrays.fill(kekBytes, (byte) 0x00);
+ }
+ }
+
+ @Override
+ public byte[] writeKeysToMasterkeyFile(CharSequence passphrase) {
+ final byte[] scryptSalt = new byte[SCRYPT_SALT_LENGTH];
+ randomSource.nextBytes(scryptSalt);
+
+ final byte[] kekBytes = Scrypt.scrypt(passphrase, scryptSalt, SCRYPT_COST_PARAM, SCRYPT_BLOCK_SIZE, KEYLENGTH_IN_BYTES);
+ final byte[] wrappedEncryptionKey;
+ final byte[] wrappedMacKey;
+ try {
+ final SecretKey kek = new SecretKeySpec(kekBytes, ENCRYPTION_ALG);
+ wrappedEncryptionKey = AesKeyWrap.wrap(kek, encryptionKey);
+ wrappedMacKey = AesKeyWrap.wrap(kek, macKey);
+ } finally {
+ Arrays.fill(kekBytes, (byte) 0x00);
+ }
+
+ final KeyFile keyfile = new KeyFile();
+ keyfile.setVersion(KeyFile.CURRENT_VERSION);
+ keyfile.setScryptSalt(scryptSalt);
+ keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
+ keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
+ keyfile.setEncryptionMasterKey(wrappedEncryptionKey);
+ keyfile.setMacMasterKey(wrappedMacKey);
+
+ try {
+ final ObjectMapper om = new ObjectMapper();
+ return om.writeValueAsBytes(keyfile);
+ } catch (JsonProcessingException e) {
+ throw new IllegalArgumentException("Unable to create JSON from " + keyfile, e);
+ }
}
/* ======================= destruction ======================= */
@@ -29,12 +143,14 @@ public class CryptorImpl implements Cryptor {
public void destroy() throws DestroyFailedException {
TheDestroyer.destroyQuietly(encryptionKey);
TheDestroyer.destroyQuietly(macKey);
- TheDestroyer.destroyQuietly(filenameCryptor);
+ if (filenameCryptor.get() != null) {
+ TheDestroyer.destroyQuietly(getFilenameCryptor());
+ }
}
@Override
public boolean isDestroyed() {
- return encryptionKey.isDestroyed() && macKey.isDestroyed() && filenameCryptor.isDestroyed();
+ return encryptionKey.isDestroyed() && macKey.isDestroyed() && (filenameCryptor.get() == null || filenameCryptor.get().isDestroyed());
}
}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
index 238034f90..ea1dfe96d 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
@@ -1,3 +1,11 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
package org.cryptomator.crypto.engine.impl;
import java.io.UncheckedIOException;
@@ -25,6 +33,9 @@ class FilenameCryptorImpl implements FilenameCryptor {
private final SecretKey macKey;
FilenameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) {
+ if (encryptionKey == null || macKey == null) {
+ throw new IllegalArgumentException("Key must not be null");
+ }
this.encryptionKey = encryptionKey;
this.macKey = macKey;
}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/KeyFile.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/KeyFile.java
new file mode 100644
index 000000000..a40c3e61e
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/KeyFile.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.crypto.engine.impl;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "primaryMasterKey", "hmacMasterKey"})
+class KeyFile implements Serializable {
+
+ static final Integer CURRENT_VERSION = 3;
+ private static final long serialVersionUID = 8578363158959619885L;
+
+ @JsonProperty("version")
+ private Integer version;
+
+ @JsonProperty("scryptSalt")
+ private byte[] scryptSalt;
+
+ @JsonProperty("scryptCostParam")
+ private int scryptCostParam;
+
+ @JsonProperty("scryptBlockSize")
+ private int scryptBlockSize;
+
+ @JsonProperty("primaryMasterKey")
+ private byte[] encryptionMasterKey;
+
+ @JsonProperty("hmacMasterKey")
+ private byte[] macMasterKey;
+
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ public byte[] getScryptSalt() {
+ return scryptSalt;
+ }
+
+ public void setScryptSalt(byte[] scryptSalt) {
+ this.scryptSalt = scryptSalt;
+ }
+
+ public int getScryptCostParam() {
+ return scryptCostParam;
+ }
+
+ public void setScryptCostParam(int scryptCostParam) {
+ this.scryptCostParam = scryptCostParam;
+ }
+
+ public int getScryptBlockSize() {
+ return scryptBlockSize;
+ }
+
+ public void setScryptBlockSize(int scryptBlockSize) {
+ this.scryptBlockSize = scryptBlockSize;
+ }
+
+ public byte[] getEncryptionMasterKey() {
+ return encryptionMasterKey;
+ }
+
+ public void setEncryptionMasterKey(byte[] encryptionMasterKey) {
+ this.encryptionMasterKey = encryptionMasterKey;
+ }
+
+ public byte[] getMacMasterKey() {
+ return macMasterKey;
+ }
+
+ public void setMacMasterKey(byte[] macMasterKey) {
+ this.macMasterKey = macMasterKey;
+ }
+
+}
\ No newline at end of file
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/Scrypt.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/Scrypt.java
new file mode 100644
index 000000000..73fedfc14
--- /dev/null
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/Scrypt.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.crypto.engine.impl;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.bouncycastle.crypto.generators.SCrypt;
+
+final class Scrypt {
+
+ private Scrypt() {
+ }
+
+ /**
+ * Derives a key from the given passphrase.
+ * This implementation makes sure, any copies of the passphrase used during key derivation are overwritten in memory asap (before next GC cycle).
+ *
+ * @param passphrase The passphrase
+ * @param salt Salt, ideally randomly generated
+ * @param costParam Cost parameter N, larger than 1, a power of 2 and less than 2^(128 * costParam / 8)
+ * @param blockSize Block size r
+ * @param keyLengthInBytes Key output length dkLen
+ * @return Derived key
+ * @see RFC Draft
+ */
+ public static byte[] scrypt(CharSequence passphrase, byte[] salt, int costParam, int blockSize, int keyLengthInBytes) {
+ // This is an attempt to get the password bytes without copies of the password being created in some dark places inside the JVM:
+ final ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(passphrase));
+ final byte[] pw = new byte[buf.remaining()];
+ buf.get(pw);
+ try {
+ return SCrypt.generate(pw, salt, costParam, blockSize, 1, keyLengthInBytes);
+ } finally {
+ Arrays.fill(pw, (byte) 0); // overwrite bytes
+ buf.rewind(); // just resets markers
+ buf.put(pw); // this is where we overwrite the actual bytes
+ }
+ }
+
+}
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/TheDestroyer.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/TheDestroyer.java
index ae7f25071..e16ddda64 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/TheDestroyer.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/TheDestroyer.java
@@ -1,3 +1,11 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
package org.cryptomator.crypto.engine.impl;
import javax.security.auth.DestroyFailedException;
diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java
index af41f917c..bf44affde 100644
--- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java
+++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java
@@ -20,21 +20,70 @@ import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
+import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
public class CryptoFileSystem extends CryptoFolder implements FileSystem {
+ private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystem.class);
private static final String DATA_ROOT_DIR = "d";
private static final String METADATA_ROOT_DIR = "m";
private static final String ROOT_DIR_FILE = "root";
- private static final String MASTERKEY_FILE = "masterkey.cryptomator";
- private static final String MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
+ private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
+ private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
private final Folder physicalRoot;
- public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor) {
+ public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) {
super(null, "", cryptor);
this.physicalRoot = physicalRoot;
+ final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
+ if (masterkeyFile.exists()) {
+ final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
+ if (!unlocked) {
+ // TODO new InvalidPassphraseException() ?
+ throw new IllegalArgumentException("Wrong passphrase.");
+ }
+ } else {
+ encryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
+ }
+ assert masterkeyFile.exists() : "A CryptoFileSystem can not exist without a masterkey file.";
+ final File backupFile = physicalRoot.file(MASTERKEY_BACKUP_FILENAME);
+ backupMasterKeyFileSilently(masterkeyFile, backupFile);
+ }
+
+ private static boolean decryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
+ try (ReadableFile file = masterkeyFile.openReadable(1, TimeUnit.SECONDS)) {
+ // TODO we need to read the whole file but can not be sure about the buffer size:
+ final ByteBuffer bigEnoughBuffer = ByteBuffer.allocate(500);
+ file.read(bigEnoughBuffer);
+ bigEnoughBuffer.flip();
+ assert bigEnoughBuffer.remaining() < bigEnoughBuffer.capacity() : "The buffer wasn't big enough.";
+ final byte[] fileContents = new byte[bigEnoughBuffer.remaining()];
+ bigEnoughBuffer.get(fileContents);
+ return cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
+ } catch (TimeoutException e) {
+ throw new UncheckedIOException(new IOException("Failed to lock masterkey file in time. " + masterkeyFile, e));
+ }
+ }
+
+ private static void encryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, CharSequence passphrase) {
+ try (WritableFile file = masterkeyFile.openWritable(1, TimeUnit.SECONDS)) {
+ final byte[] fileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
+ file.write(ByteBuffer.wrap(fileContents));
+ } catch (TimeoutException e) {
+ throw new UncheckedIOException(new IOException("Failed to lock masterkey file in time. " + masterkeyFile, e));
+ }
+ }
+
+ private static void backupMasterKeyFileSilently(File masterkeyFile, File backupFile) {
+ try (ReadableFile src = masterkeyFile.openReadable(1, TimeUnit.SECONDS); WritableFile dst = backupFile.openWritable(1, TimeUnit.SECONDS)) {
+ src.copyTo(dst);
+ } catch (TimeoutException e) {
+ LOG.warn("Failed to lock masterkey file (" + masterkeyFile + ") or backup file (" + backupFile + ") in time. Skipping backup.");
+ }
}
@Override
@@ -77,7 +126,7 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes());
writable.write(buf);
} catch (TimeoutException e) {
- throw new UncheckedIOException(new IOException("Failed to lock directory file in time." + dirFile, e));
+ throw new UncheckedIOException(new IOException("Failed to lock directory file in time. " + dirFile, e));
}
physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS);
}
diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java
index 3bbc6ec88..1bc5da59e 100644
--- a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java
+++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java
@@ -17,4 +17,21 @@ public class NoCryptor implements Cryptor {
return filenameCryptor;
}
+ @Override
+ public void randomizeMasterkey() {
+ // like this? https://xkcd.com/221/
+ }
+
+ @Override
+ public boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase) {
+ // thanks, but I don't need a key, if I'm not encryption anything...
+ return true;
+ }
+
+ @Override
+ public byte[] writeKeysToMasterkeyFile(CharSequence passphrase) {
+ // ok, if you insist to get my non-existing key data... here you go:
+ return new byte[0];
+ }
+
}
diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java
new file mode 100644
index 000000000..98a33d914
--- /dev/null
+++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.crypto.engine.impl;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import org.cryptomator.crypto.engine.Cryptor;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CryptorImplTest {
+
+ private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
+
+ private static final long serialVersionUID = 1505563778398085504L;
+
+ @Override
+ public void nextBytes(byte[] bytes) {
+ Arrays.fill(bytes, (byte) 0x00);
+ }
+
+ };
+
+ @Test(timeout = 1000)
+ public void testMasterkeyDecryption() throws IOException {
+ final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
+ final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
+ Assert.assertFalse(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe"));
+ Assert.assertTrue(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd"));
+ }
+
+ @Test(timeout = 5000)
+ public void testMasterkeyEncryption() throws IOException {
+ final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ + "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ + "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"}";
+ final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
+ cryptor.randomizeMasterkey();
+ final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
+ Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
+ }
+
+}
diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java
index bbead6366..251fa1835 100644
--- a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java
+++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java
@@ -1,3 +1,11 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Sebastian Stenzel and others.
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
package org.cryptomator.crypto.engine.impl;
import java.io.IOException;
@@ -6,7 +14,7 @@ import java.util.UUID;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
-import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.FilenameCryptor;
import org.junit.Assert;
import org.junit.Test;
@@ -17,24 +25,24 @@ public class FilenameCryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
- final Cryptor cryptor = new CryptorImpl(encryptionKey, macKey);
+ final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
// some random
for (int i = 0; i < 2000; i++) {
final String origName = UUID.randomUUID().toString();
- final String encrypted1 = cryptor.getFilenameCryptor().encryptFilename(origName);
- final String encrypted2 = cryptor.getFilenameCryptor().encryptFilename(origName);
+ final String encrypted1 = filenameCryptor.encryptFilename(origName);
+ final String encrypted2 = filenameCryptor.encryptFilename(origName);
Assert.assertEquals(encrypted1, encrypted2);
- final String decrypted = cryptor.getFilenameCryptor().decryptFilename(encrypted1);
+ final String decrypted = filenameCryptor.decryptFilename(encrypted1);
Assert.assertEquals(origName, decrypted);
}
// block size length file names
final String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii
- final String encryptedPath3a = cryptor.getFilenameCryptor().encryptFilename(originalPath3);
- final String encryptedPath3b = cryptor.getFilenameCryptor().encryptFilename(originalPath3);
+ final String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3);
+ final String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
- final String decryptedPath3 = cryptor.getFilenameCryptor().decryptFilename(encryptedPath3a);
+ final String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a);
Assert.assertEquals(originalPath3, decryptedPath3);
}
@@ -43,13 +51,13 @@ public class FilenameCryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
- final Cryptor cryptor = new CryptorImpl(encryptionKey, macKey);
+ final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
// some random
for (int i = 0; i < 2000; i++) {
final String originalDirectoryId = UUID.randomUUID().toString();
- final String hashedDirectory1 = cryptor.getFilenameCryptor().hashDirectoryId(originalDirectoryId);
- final String hashedDirectory2 = cryptor.getFilenameCryptor().hashDirectoryId(originalDirectoryId);
+ final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId);
+ final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId);
Assert.assertEquals(hashedDirectory1, hashedDirectory2);
}
}
diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
index 55b887681..20b8bfa29 100644
--- a/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
+++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
@@ -10,10 +10,12 @@ package org.cryptomator.crypto.fs;
import java.io.IOException;
import java.io.UncheckedIOException;
+import java.time.Instant;
import java.util.concurrent.atomic.AtomicInteger;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.NoCryptor;
+import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
@@ -34,27 +36,59 @@ public class CryptoFileSystemTest {
// some mock fs:
final FileSystem physicalFs = new InMemoryFileSystem();
+ final File masterkeyFile = physicalFs.file("masterkey.cryptomator");
+ final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
final Folder physicalDataRoot = physicalFs.folder("d");
+ Assert.assertFalse(masterkeyFile.exists());
+ Assert.assertFalse(masterkeyBkupFile.exists());
Assert.assertFalse(physicalDataRoot.exists());
// init crypto fs:
- final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+ final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+ Assert.assertTrue(masterkeyFile.exists());
+ Assert.assertTrue(masterkeyBkupFile.exists());
fs.create(FolderCreateMode.INCLUDING_PARENTS);
Assert.assertTrue(physicalDataRoot.exists());
- Assert.assertEquals(physicalFs.children().count(), 2);
+ Assert.assertEquals(4, physicalFs.children().count()); // d + m + masterkey.cryptomator + masterkey.cryptomator.bkup
Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory
LOG.debug(DirectoryPrinter.print(physicalFs));
}
+ @Test
+ public void testMasterkeyBackupBehaviour() throws InterruptedException {
+ // mock cryptor:
+ final Cryptor cryptor = new NoCryptor();
+
+ // some mock fs:
+ final FileSystem physicalFs = new InMemoryFileSystem();
+ final File masterkeyBkupFile = physicalFs.file("masterkey.cryptomator.bkup");
+ Assert.assertFalse(masterkeyBkupFile.exists());
+
+ // first initialization:
+ new CryptoFileSystem(physicalFs, cryptor, "foo");
+ Assert.assertTrue(masterkeyBkupFile.exists());
+ final Instant bkupDateT0 = masterkeyBkupFile.lastModified();
+
+ // make sure some time passes, as the resolution of last modified date is not in nanos:
+ Thread.sleep(1);
+
+ // second initialization:
+ new CryptoFileSystem(physicalFs, cryptor, "foo");
+ Assert.assertTrue(masterkeyBkupFile.exists());
+ final Instant bkupDateT1 = masterkeyBkupFile.lastModified();
+
+ Assert.assertTrue(bkupDateT1.isAfter(bkupDateT0));
+ }
+
@Test
public void testDirectoryCreation() throws UncheckedIOException, IOException {
// mock stuff and prepare crypto FS:
final Cryptor cryptor = new NoCryptor();
final FileSystem physicalFs = new InMemoryFileSystem();
final Folder physicalDataRoot = physicalFs.folder("d");
- final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+ final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
fs.create(FolderCreateMode.INCLUDING_PARENTS);
// add another encrypted folder:
@@ -75,7 +109,7 @@ public class CryptoFileSystemTest {
// mock stuff and prepare crypto FS:
final Cryptor cryptor = new NoCryptor();
final FileSystem physicalFs = new InMemoryFileSystem();
- final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+ final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
fs.create(FolderCreateMode.INCLUDING_PARENTS);
// create foo/bar/ and then move foo/ to baz/:
@@ -98,7 +132,7 @@ public class CryptoFileSystemTest {
// mock stuff and prepare crypto FS:
final Cryptor cryptor = new NoCryptor();
final FileSystem physicalFs = new InMemoryFileSystem();
- final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor);
+ final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
fs.create(FolderCreateMode.INCLUDING_PARENTS);
// create foo/bar/ and then try to move foo/bar/ to foo/
diff --git a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
index 94714824a..69f7194f6 100644
--- a/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
+++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
@@ -129,6 +129,7 @@ class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile {
@Override
public void close() {
if (lock.isWriteLockedByCurrentThread()) {
+ this.setLastModified(Instant.now());
lock.writeLock().unlock();
} else if (lock.getReadHoldCount() > 0) {
lock.readLock().unlock();
diff --git a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java
index d51ec5091..2a12e4c6b 100644
--- a/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java
+++ b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java
@@ -8,7 +8,9 @@
*******************************************************************************/
package org.cryptomator.filesystem.inmem;
+import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
+import java.time.Instant;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -52,6 +54,40 @@ public class InMemoryFileSystemTest {
Assert.assertEquals(1, fooFolder.folders().count());
}
+ @Test
+ public void testImplicitUpdateOfModifiedDateAfterWrite() throws UncheckedIOException, TimeoutException, InterruptedException {
+ final FileSystem fs = new InMemoryFileSystem();
+ File fooFile = fs.file("foo.txt");
+
+ final Instant beforeFirstModification = Instant.now();
+
+ Thread.sleep(1);
+
+ // write "hello world" to foo
+ try (WritableFile writable = fooFile.openWritable(1, TimeUnit.SECONDS)) {
+ writable.write(ByteBuffer.wrap("hello world".getBytes()));
+ }
+ Assert.assertTrue(fooFile.exists());
+ final Instant firstModification = fooFile.lastModified();
+
+ Thread.sleep(1);
+
+ final Instant afterFirstModification = Instant.now();
+ Assert.assertTrue(beforeFirstModification.isBefore(firstModification));
+ Assert.assertTrue(afterFirstModification.isAfter(firstModification));
+
+ Thread.sleep(1);
+
+ // write "dlrow olleh" to foo
+ try (WritableFile writable = fooFile.openWritable(1, TimeUnit.SECONDS)) {
+ writable.write(ByteBuffer.wrap("dlrow olleh".getBytes()));
+ }
+ Assert.assertTrue(fooFile.exists());
+ final Instant secondModification = fooFile.lastModified();
+
+ Assert.assertTrue(firstModification.isBefore(secondModification));
+ }
+
@Test
public void testFileReadCopyMoveWrite() throws TimeoutException {
final FileSystem fs = new InMemoryFileSystem();