From 70eb0c99e4795bfd0622566d960a16195ad87684 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 15 Dec 2015 19:50:42 +0100 Subject: [PATCH] implemented encryption/decryption of masterkey file in crypto layer --- main/crypto-layer/pom.xml | 6 + .../crypto/engine/CryptoException.java | 8 ++ .../cryptomator/crypto/engine/Cryptor.java | 6 + .../crypto/engine/impl/AesKeyWrap.java | 71 +++++++++ .../crypto/engine/impl/CryptorImpl.java | 136 ++++++++++++++++-- .../engine/impl/FilenameCryptorImpl.java | 11 ++ .../crypto/engine/impl/KeyFile.java | 88 ++++++++++++ .../crypto/engine/impl/Scrypt.java | 49 +++++++ .../crypto/engine/impl/TheDestroyer.java | 8 ++ .../crypto/fs/CryptoFileSystem.java | 57 +++++++- .../cryptomator/crypto/engine/NoCryptor.java | 17 +++ .../crypto/engine/impl/CryptorImplTest.java | 53 +++++++ .../engine/impl/FilenameCryptorImplTest.java | 30 ++-- .../crypto/fs/CryptoFileSystemTest.java | 44 +++++- .../filesystem/inmem/InMemoryFile.java | 1 + .../inmem/InMemoryFileSystemTest.java | 36 +++++ 16 files changed, 591 insertions(+), 30 deletions(-) create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/AesKeyWrap.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/KeyFile.java create mode 100644 main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/Scrypt.java create mode 100644 main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java 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();