diff --git a/main/crypto-layer/pom.xml b/main/crypto-layer/pom.xml index 4f74e5233..5fe215908 100644 --- a/main/crypto-layer/pom.xml +++ b/main/crypto-layer/pom.xml @@ -1,6 +1,6 @@ + + org.cryptomator + siv-mode + ${sivmode.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncycastle.version} + + + org.apache.commons commons-lang3 @@ -31,5 +49,12 @@ commons-codec commons-codec + + + + org.cryptomator + filesystem-inmemory + test + \ No newline at end of file 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 new file mode 100644 index 000000000..42054592c --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/CryptoException.java @@ -0,0 +1,13 @@ +package org.cryptomator.crypto.engine; + +import java.io.IOException; + +public class CryptoException extends IOException { + + private static final long serialVersionUID = -6536997506620449023L; + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + +} 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 new file mode 100644 index 000000000..c1cc321e3 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/Cryptor.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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 javax.security.auth.Destroyable; + +/** + * A Cryptor instance, once initialized with a set of keys, provides access to threadsafe cryptographic routines. + */ +public interface Cryptor extends Destroyable { + + FilenameCryptor getFilenameCryptor(); + +} diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java new file mode 100644 index 000000000..38e4c3c19 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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 javax.security.auth.Destroyable; + +/** + * Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts, + * otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption. + * + * @see Wikipedia on deterministic encryption + */ +public interface FilenameCryptor extends Destroyable { + + /** + * @return constant length string, that is unlikely to collide with any other name. + */ + String hashDirectoryId(String cleartextDirectoryId); + + /** + * @param cleartextName original filename including cleartext file extension + * @return encrypted filename without any file extension + */ + String encryptFilename(String cleartextName); + + /** + * @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first. + * @return cleartext filename, probably including its cleartext file extension. + */ + String decryptFilename(String ciphertextName); +} 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 new file mode 100644 index 000000000..f6dfd94b0 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java @@ -0,0 +1,40 @@ +package org.cryptomator.crypto.engine.impl; + +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +import org.cryptomator.crypto.engine.Cryptor; +import org.cryptomator.crypto.engine.FilenameCryptor; + +public class CryptorImpl implements Cryptor { + + private final SecretKey encryptionKey; + private final SecretKey macKey; + private final FilenameCryptor filenameCryptor; + + public CryptorImpl(SecretKey encryptionKey, SecretKey macKey) { + this.encryptionKey = encryptionKey; + this.macKey = macKey; + this.filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey); + } + + @Override + public FilenameCryptor getFilenameCryptor() { + return filenameCryptor; + } + + /* ======================= destruction ======================= */ + + @Override + public void destroy() throws DestroyFailedException { + TheDestroyer.destroyQuietly(encryptionKey); + TheDestroyer.destroyQuietly(macKey); + TheDestroyer.destroyQuietly(filenameCryptor); + } + + @Override + public boolean isDestroyed() { + return encryptionKey.isDestroyed() && macKey.isDestroyed() && filenameCryptor.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 new file mode 100644 index 000000000..238034f90 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -0,0 +1,90 @@ +package org.cryptomator.crypto.engine.impl; + +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import javax.security.auth.DestroyFailedException; + +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.BaseNCodec; +import org.cryptomator.crypto.engine.CryptoException; +import org.cryptomator.crypto.engine.FilenameCryptor; +import org.cryptomator.siv.SivMode; + +class FilenameCryptorImpl implements FilenameCryptor { + + private static final BaseNCodec BASE32 = new Base32(); + private static final ThreadLocal SHA256 = new ThreadLocalSha256(); + private static final SivMode AES_SIV = new SivMode(); + + private final SecretKey encryptionKey; + private final SecretKey macKey; + + FilenameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) { + this.encryptionKey = encryptionKey; + this.macKey = macKey; + } + + @Override + public String hashDirectoryId(String cleartextDirectoryId) { + final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8); + byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes); + final byte[] hashedBytes = SHA256.get().digest(encryptedBytes); + return BASE32.encodeAsString(hashedBytes); + } + + @Override + public String encryptFilename(String cleartextName) { + final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8); + final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes); + return BASE32.encodeAsString(encryptedBytes); + } + + @Override + public String decryptFilename(String ciphertextName) { + final byte[] encryptedBytes = BASE32.decode(ciphertextName); + try { + final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes); + return new String(cleartextBytes, StandardCharsets.UTF_8); + } catch (AEADBadTagException e) { + throw new UncheckedIOException(new CryptoException("Authentication failed.", e)); + } + } + + private static class ThreadLocalSha256 extends ThreadLocal { + + @Override + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-256 exists in every JVM"); + } + } + + @Override + public MessageDigest get() { + final MessageDigest messageDigest = super.get(); + messageDigest.reset(); + return messageDigest; + } + } + + /* ======================= destruction ======================= */ + + @Override + public void destroy() throws DestroyFailedException { + TheDestroyer.destroyQuietly(encryptionKey); + TheDestroyer.destroyQuietly(macKey); + } + + @Override + public boolean isDestroyed() { + return encryptionKey.isDestroyed() && macKey.isDestroyed(); + } + +} 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 new file mode 100644 index 000000000..ae7f25071 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/impl/TheDestroyer.java @@ -0,0 +1,20 @@ +package org.cryptomator.crypto.engine.impl; + +import javax.security.auth.DestroyFailedException; +import javax.security.auth.Destroyable; + +final class TheDestroyer { + + private TheDestroyer() { + + } + + public static void destroyQuietly(Destroyable d) { + try { + d.destroy(); + } catch (DestroyFailedException e) { + // ignore + } + } + +} diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/package-info.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/package-info.java new file mode 100644 index 000000000..ed4065919 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/engine/package-info.java @@ -0,0 +1,12 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ +/** + * This is where the actual encryption, decryption, hashing and authenticating takes place. + */ +package org.cryptomator.crypto.engine; \ No newline at end of file diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFile.java similarity index 57% rename from main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java rename to main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFile.java index 1a64d402c..769ed35ef 100644 --- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFile.java +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFile.java @@ -1,4 +1,12 @@ -package org.cryptomator.crypto; +/******************************************************************************* + * 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.fs; import java.io.IOException; import java.io.UncheckedIOException; @@ -6,21 +14,22 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; public class CryptoFile extends CryptoNode implements File { - private static final String ENCRYPTED_FILE_EXT = ".file"; + static final String FILE_EXT = ".file"; - public CryptoFile(CryptoFolder parent, String name) { - super(parent, name); + public CryptoFile(CryptoFolder parent, String name, Cryptor cryptor) { + super(parent, name, cryptor); } @Override String encryptedName() { - return name() + ENCRYPTED_FILE_EXT; + return name() + FILE_EXT; } @Override diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java similarity index 54% rename from main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java rename to main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java index e03c72d48..59c7f2e4b 100644 --- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFileSystem.java +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFileSystem.java @@ -1,12 +1,25 @@ -package org.cryptomator.crypto; +/******************************************************************************* + * 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.fs; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; +import org.cryptomator.filesystem.WritableFile; public class CryptoFileSystem extends CryptoFolder implements FileSystem { @@ -18,8 +31,8 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem { private final Folder physicalRoot; - public CryptoFileSystem(Folder physicalRoot) { - super(null, ""); + public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor) { + super(null, "", cryptor); this.physicalRoot = physicalRoot; } @@ -28,12 +41,6 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem { return physicalDataRoot().file(ROOT_DIR_FILE); } - @Override - Folder physicalFolder() throws IOException { - // TODO Auto-generated method stub - return super.physicalFolder(); - } - @Override Folder physicalDataRoot() { return physicalRoot.folder(DATA_ROOT_DIR); @@ -68,7 +75,15 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem { public void create(FolderCreateMode mode) throws IOException { physicalDataRoot().create(mode); physicalMetadataRoot().create(mode); - super.create(mode); + final File dirFile = physicalFile(); + final String directoryId = getDirectoryId(); + try (WritableFile writable = dirFile.openWritable(1, TimeUnit.SECONDS)) { + final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes()); + writable.write(buf); + } catch (TimeoutException e) { + throw new IOException("Failed to lock directory file in time." + dirFile, e); + } + physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS); } } diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java similarity index 63% rename from main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java rename to main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java index d45109568..d5f5db866 100644 --- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoFolder.java +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoFolder.java @@ -1,5 +1,14 @@ -package org.cryptomator.crypto; +/******************************************************************************* + * 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.fs; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; @@ -14,6 +23,7 @@ import java.util.stream.Stream; import org.apache.commons.codec.binary.Base32; import org.apache.commons.lang3.StringUtils; +import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; @@ -23,18 +33,17 @@ import org.cryptomator.filesystem.WritableFile; class CryptoFolder extends CryptoNode implements Folder { - private static final String ENCRYPTED_FILE_EXT = ".file"; - private static final String ENCRYPTED_DIR_EXT = ".dir"; + static final String FILE_EXT = ".dir"; private final AtomicReference directoryId = new AtomicReference<>(); - public CryptoFolder(CryptoFolder parent, String name) { - super(parent, name); + public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) { + super(parent, name, cryptor); } @Override String encryptedName() { - return name() + ENCRYPTED_DIR_EXT; + return name() + FILE_EXT; } protected String getDirectoryId() throws IOException { @@ -92,32 +101,32 @@ class CryptoFolder extends CryptoNode implements Folder { @Override public Stream files() throws IOException { - return physicalFolder().files().map(File::name).filter(s -> s.endsWith(ENCRYPTED_FILE_EXT)).map(this::decryptFileName).map(this::file); + return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptFileName).map(this::file); } private String decryptFileName(String encryptedFileName) { - // TODO Auto-generated method stub - return StringUtils.removeEnd(encryptedFileName, ENCRYPTED_FILE_EXT); + final String ciphertext = StringUtils.removeEnd(encryptedFileName, CryptoFile.FILE_EXT); + return cryptor.getFilenameCryptor().decryptFilename(ciphertext); } @Override public CryptoFile file(String name) { - return new CryptoFile(this, name); + return new CryptoFile(this, name, cryptor); } @Override public Stream folders() throws IOException { - return physicalFolder().files().map(File::name).filter(s -> s.endsWith(ENCRYPTED_DIR_EXT)).map(this::decryptFolderName).map(this::folder); + return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.FILE_EXT)).map(this::decryptFolderName).map(this::folder); } private String decryptFolderName(String encryptedFolderName) { - // TODO Auto-generated method stub - return StringUtils.removeEnd(encryptedFolderName, ENCRYPTED_DIR_EXT); + final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.FILE_EXT); + return cryptor.getFilenameCryptor().decryptFilename(ciphertext); } @Override public CryptoFolder folder(String name) { - return new CryptoFolder(this, name); + return new CryptoFolder(this, name, cryptor); } @Override @@ -125,16 +134,21 @@ class CryptoFolder extends CryptoNode implements Folder { final File dirFile = physicalFile(); if (dirFile.exists()) { return; - } else { - final String directoryId = getDirectoryId(); - try (WritableFile writable = dirFile.openWritable(1, TimeUnit.SECONDS)) { - final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes()); - writable.write(buf); - } catch (TimeoutException e) { - throw new IOException("Failed to lock directory file in time." + dirFile, e); - } - physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS); } + if (!parent.exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) { + throw new FileNotFoundException(parent.name); + } else if (!parent.exists() && FolderCreateMode.INCLUDING_PARENTS.equals(mode)) { + parent.create(mode); + } + assert parent.exists(); + final String directoryId = getDirectoryId(); + try (WritableFile writable = dirFile.openWritable(1, TimeUnit.SECONDS)) { + final ByteBuffer buf = ByteBuffer.wrap(directoryId.getBytes()); + writable.write(buf); + } catch (TimeoutException e) { + throw new IOException("Failed to lock directory file in time." + dirFile, e); + } + physicalFolder().create(FolderCreateMode.INCLUDING_PARENTS); } @Override diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoNode.java similarity index 70% rename from main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java rename to main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoNode.java index dc38a2a16..1fd6ca6ca 100644 --- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/CryptoNode.java +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/CryptoNode.java @@ -1,9 +1,18 @@ -package org.cryptomator.crypto; +/******************************************************************************* + * 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.fs; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Optional; +import org.cryptomator.crypto.engine.Cryptor; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.Node; @@ -11,10 +20,12 @@ abstract class CryptoNode implements Node { protected final CryptoFolder parent; protected final String name; + protected final Cryptor cryptor; - public CryptoNode(CryptoFolder parent, String name) { + public CryptoNode(CryptoFolder parent, String name, Cryptor cryptor) { this.parent = parent; this.name = name; + this.cryptor = cryptor; } Folder physicalDataRoot() { diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/package-info.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/package-info.java new file mode 100644 index 000000000..e7027fbb1 --- /dev/null +++ b/main/crypto-layer/src/main/java/org/cryptomator/crypto/fs/package-info.java @@ -0,0 +1,14 @@ +/******************************************************************************* + * 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 + *******************************************************************************/ +/** + * Provides a decoration layer for the {@link org.cryptomator.filesystem Filesystem API}, consuming an encrypted file system and providing access to a cleartext filesystem. + * While the implementation in this package dictates the Vault directory layout, no encryption code can be found here. + * All cryptographic operations are delegated to the {@link org.cryptomator.crypto.engine CryptoEngine}. + */ +package org.cryptomator.crypto.fs; \ No newline at end of file diff --git a/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java b/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java deleted file mode 100644 index 765e4fefa..000000000 --- a/main/crypto-layer/src/main/java/org/cryptomator/crypto/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Provides a decoration layer for the {@link org.cryptomator.filesystem} API, consuming an encrypted file system and providing access to a cleartext filesystem. - */ -package org.cryptomator.crypto; \ No newline at end of file diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java deleted file mode 100644 index 4f459df8f..000000000 --- a/main/crypto-layer/src/test/java/org/cryptomator/crypto/CryptoFileSystemTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.cryptomator.crypto; - -import java.io.IOException; -import java.io.UncheckedIOException; - -import org.cryptomator.filesystem.FileSystem; -import org.cryptomator.filesystem.Folder; -import org.cryptomator.filesystem.FolderCreateMode; -import org.cryptomator.filesystem.inmem.InMemoryFileSystem; -import org.junit.Assert; -import org.junit.Test; - -public class CryptoFileSystemTest { - - @Test - public void testFilenameEncryption() throws UncheckedIOException, IOException { - // some mock fs: - FileSystem physicalFs = new InMemoryFileSystem(); - Folder dataRoot = physicalFs.folder("d"); - Assert.assertFalse(dataRoot.exists()); - - // init crypto fs: - FileSystem fs = new CryptoFileSystem(physicalFs); - fs.create(FolderCreateMode.INCLUDING_PARENTS); - Assert.assertTrue(dataRoot.exists()); - Assert.assertEquals(physicalFs.children().count(), 2); - Assert.assertEquals(1, dataRoot.files().count()); // ROOT file - Assert.assertEquals(1, dataRoot.folders().count()); // ROOT directory - - // add another encrypted folder: - Folder testFolder = fs.folder("test"); - Assert.assertFalse(testFolder.exists()); - testFolder.create(FolderCreateMode.INCLUDING_PARENTS); - Assert.assertTrue(testFolder.exists()); - Assert.assertEquals(2, dataRoot.folders().count()); - } - -} 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 new file mode 100644 index 000000000..3bbc6ec88 --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoCryptor.java @@ -0,0 +1,20 @@ +/******************************************************************************* + * 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; + +public class NoCryptor implements Cryptor { + + private final FilenameCryptor filenameCryptor = new NoFilenameCryptor(); + + @Override + public FilenameCryptor getFilenameCryptor() { + return filenameCryptor; + } + +} diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java new file mode 100644 index 000000000..2f5a8c482 --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java @@ -0,0 +1,59 @@ +/******************************************************************************* + * 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.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Base32; +import org.apache.commons.codec.binary.BaseNCodec; + +class NoFilenameCryptor implements FilenameCryptor { + + private static final BaseNCodec BASE32 = new Base32(); + private static final ThreadLocal SHA1 = new ThreadLocalSha1(); + + @Override + public String hashDirectoryId(String cleartextDirectoryId) { + final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8); + final byte[] hashedBytes = SHA1.get().digest(cleartextBytes); + return BASE32.encodeAsString(hashedBytes); + } + + @Override + public String encryptFilename(String cleartextName) { + return cleartextName; + } + + @Override + public String decryptFilename(String ciphertextName) { + return ciphertextName; + } + + private static class ThreadLocalSha1 extends ThreadLocal { + + @Override + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("SHA-1 exists in every JVM"); + } + } + + @Override + public MessageDigest get() { + final MessageDigest sha1 = super.get(); + sha1.reset(); + return sha1; + } + } + +} 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 new file mode 100644 index 000000000..bbead6366 --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java @@ -0,0 +1,57 @@ +package org.cryptomator.crypto.engine.impl; + +import java.io.IOException; +import java.util.UUID; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.cryptomator.crypto.engine.Cryptor; +import org.junit.Assert; +import org.junit.Test; + +public class FilenameCryptorImplTest { + + @Test(timeout = 1000) + public void testDeterministicEncryptionOfFilenames() throws IOException { + 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); + + // 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); + Assert.assertEquals(encrypted1, encrypted2); + final String decrypted = cryptor.getFilenameCryptor().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); + Assert.assertEquals(encryptedPath3a, encryptedPath3b); + final String decryptedPath3 = cryptor.getFilenameCryptor().decryptFilename(encryptedPath3a); + Assert.assertEquals(originalPath3, decryptedPath3); + } + + @Test(timeout = 1000) + public void testDeterministicHashingOfDirectoryIds() throws IOException { + 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); + + // 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); + 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 new file mode 100644 index 000000000..2a805fb1b --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java @@ -0,0 +1,79 @@ +/******************************************************************************* + * 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.fs; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.concurrent.atomic.AtomicInteger; + +import org.cryptomator.crypto.engine.Cryptor; +import org.cryptomator.crypto.engine.NoCryptor; +import org.cryptomator.filesystem.FileSystem; +import org.cryptomator.filesystem.Folder; +import org.cryptomator.filesystem.FolderCreateMode; +import org.cryptomator.filesystem.inmem.InMemoryFileSystem; +import org.junit.Assert; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CryptoFileSystemTest { + + private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemTest.class); + + @Test + public void testFilenameEncryption() throws UncheckedIOException, IOException { + // mock cryptor: + Cryptor cryptor = new NoCryptor(); + + // some mock fs: + FileSystem physicalFs = new InMemoryFileSystem(); + Folder physicalDataRoot = physicalFs.folder("d"); + Assert.assertFalse(physicalDataRoot.exists()); + + // init crypto fs: + FileSystem fs = new CryptoFileSystem(physicalFs, cryptor); + fs.create(FolderCreateMode.INCLUDING_PARENTS); + Assert.assertTrue(physicalDataRoot.exists()); + Assert.assertEquals(physicalFs.children().count(), 2); + Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file + Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory + + // add another encrypted folder: + Folder fooFolder = fs.folder("foo"); + Folder barFolder = fooFolder.folder("bar"); + Assert.assertFalse(fooFolder.exists()); + Assert.assertFalse(barFolder.exists()); + barFolder.create(FolderCreateMode.INCLUDING_PARENTS); + Assert.assertTrue(fooFolder.exists()); + Assert.assertTrue(barFolder.exists()); + Assert.assertEquals(3, countDataFolders(physicalDataRoot)); // parent + foo + bar + + LOG.info(DirectoryPrinter.print(fs)); + LOG.info(DirectoryPrinter.print(physicalFs)); + } + + /** + * @return number of folders on second level inside the given dataRoot folder. + */ + private static int countDataFolders(Folder dataRoot) { + final AtomicInteger num = new AtomicInteger(); + DirectoryWalker.walk(dataRoot, 0, 2, (node) -> { + if (node instanceof Folder) { + final Folder nodeParent = node.parent().get(); + final Folder nodeParentParent = nodeParent.parent().orElse(null); + if (nodeParentParent != null && nodeParentParent.equals(dataRoot)) { + num.incrementAndGet(); + } + } + }); + return num.get(); + } + +} diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryPrinter.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryPrinter.java new file mode 100644 index 000000000..85859b1eb --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryPrinter.java @@ -0,0 +1,40 @@ +/******************************************************************************* + * 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.fs; + +import java.util.Optional; + +import org.cryptomator.filesystem.File; +import org.cryptomator.filesystem.Folder; + +public final class DirectoryPrinter { + + private DirectoryPrinter() { + } + + public static String print(Folder folder) { + StringBuilder sb = new StringBuilder(folder.name()).append('\n'); + + DirectoryWalker.walk(folder, (node) -> { + Optional parent = node.parent(); + while (parent.isPresent()) { + sb.append(" "); + parent = parent.get().parent(); + } + if (node instanceof Folder) { + sb.append(node.name()).append('/').append('\n'); + } else if (node instanceof File) { + sb.append(node.name()).append('\n'); + } + }); + + return sb.toString(); + } + +} diff --git a/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryWalker.java b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryWalker.java new file mode 100644 index 000000000..497468718 --- /dev/null +++ b/main/crypto-layer/src/test/java/org/cryptomator/crypto/fs/DirectoryWalker.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * 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.fs; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.function.Consumer; + +import org.cryptomator.filesystem.Folder; +import org.cryptomator.filesystem.Node; + +final class DirectoryWalker { + + private DirectoryWalker() { + } + + public static void walk(Folder folder, Consumer visitor) { + walk(folder, 0, Integer.MAX_VALUE, visitor); + } + + public static void walk(Folder folder, int depth, int maxDepth, Consumer visitor) { + try { + folder.files().forEach(visitor); + if (depth == maxDepth) { + return; + } else { + folder.folders().forEach(childFolder -> { + visitor.accept(childFolder); + walk(childFolder, depth + 1, maxDepth, visitor); + }); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + +} diff --git a/main/crypto-layer/src/test/resources/log4j2.xml b/main/crypto-layer/src/test/resources/log4j2.xml new file mode 100644 index 000000000..9b4889392 --- /dev/null +++ b/main/crypto-layer/src/test/resources/log4j2.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/filesystem-inmemory/.gitignore b/main/filesystem-inmemory/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/main/filesystem-inmemory/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/main/filesystem-inmemory/pom.xml b/main/filesystem-inmemory/pom.xml new file mode 100644 index 000000000..eca6b433f --- /dev/null +++ b/main/filesystem-inmemory/pom.xml @@ -0,0 +1,26 @@ + + + + 4.0.0 + + org.cryptomator + main + 0.11.0-SNAPSHOT + + filesystem-inmemory + Cryptomator in-memory filesystem + + + + org.cryptomator + filesystem-api + + + \ No newline at end of file diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java similarity index 86% rename from main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java rename to main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java index a6cc92070..90963682a 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.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.filesystem.inmem; import java.io.FileNotFoundException; @@ -12,12 +20,12 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import org.cryptomator.filesystem.ReadableFile; import org.cryptomator.filesystem.WritableFile; -public class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile { +class InMemoryFile extends InMemoryNode implements ReadableFile, WritableFile { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private ByteBuffer content = ByteBuffer.wrap(new byte[0]); - InMemoryFile(InMemoryFolder parent, String name, Instant lastModified) { + public InMemoryFile(InMemoryFolder parent, String name, Instant lastModified) { super(parent, name, lastModified); } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java similarity index 57% rename from main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java rename to main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java index 86f403a86..bc1b8ce3f 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFileSystem.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.filesystem.inmem; import java.time.Instant; diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java similarity index 82% rename from main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java rename to main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java index 6bb1cbbec..114b667f2 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.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.filesystem.inmem; import java.io.FileNotFoundException; @@ -14,12 +22,12 @@ import org.apache.commons.io.FileExistsException; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; -public class InMemoryFolder extends InMemoryNode implements Folder { +class InMemoryFolder extends InMemoryNode implements Folder { final Map children = new TreeMap<>(); final Map volatileChildren = new HashMap<>(); - InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) { + public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) { super(parent, name, lastModified); } diff --git a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java similarity index 71% rename from main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java rename to main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java index 53dd7d833..fbe899529 100644 --- a/main/filesystem-api/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.java +++ b/main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryNode.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.filesystem.inmem; import java.time.Instant; @@ -5,13 +13,13 @@ import java.util.Optional; import org.cryptomator.filesystem.Node; -public class InMemoryNode implements Node { +class InMemoryNode implements Node { protected final InMemoryFolder parent; protected final String name; protected Instant lastModified; - InMemoryNode(InMemoryFolder parent, String name, Instant lastModified) { + public InMemoryNode(InMemoryFolder parent, String name, Instant lastModified) { this.parent = parent; this.name = name; this.lastModified = lastModified; diff --git a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java similarity index 88% rename from main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java rename to main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java index 3b04ac297..563a64180 100644 --- a/main/filesystem-api/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.java +++ b/main/filesystem-inmemory/src/test/java/org/cryptomator/filesystem/inmem/InMemoryFileSystemTest.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.filesystem.inmem; import java.io.IOException; diff --git a/main/pom.xml b/main/pom.xml index 54732d1a9..c97d31d9c 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -56,12 +56,23 @@ org.cryptomator - core + filesystem-api ${project.version} org.cryptomator - filesystem-api + filesystem-inmemory + ${project.version} + + + org.cryptomator + crypto-layer + ${project.version} + + + + org.cryptomator + core ${project.version} @@ -198,11 +209,12 @@ filesystem-api + filesystem-inmemory + crypto-layer crypto-api crypto-aes core ui - crypto-layer