began implementation of new filesystem api based encryption layer

This commit is contained in:
Sebastian Stenzel
2015-12-14 19:20:00 +01:00
parent e1b74ce312
commit 99015680b1
30 changed files with 771 additions and 94 deletions
+28 -3
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
Copyright (c) 2015 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
@@ -15,14 +15,32 @@
<version>0.11.0-SNAPSHOT</version>
</parent>
<artifactId>crypto-layer</artifactId>
<name>Crypto Layer</name>
<name>Cryptomator encrypted filesystem layer</name>
<properties>
<bouncycastle.version>1.51</bouncycastle.version>
<sivmode.version>1.0.2</sivmode.version>
</properties>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<!-- Crypto -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>siv-mode</artifactId>
<version>${sivmode.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@@ -31,5 +49,12 @@
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -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);
}
}
@@ -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();
}
@@ -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 <a href="https://en.wikipedia.org/wiki/Deterministic_encryption">Wikipedia on deterministic encryption</a>
*/
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);
}
@@ -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();
}
}
@@ -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<MessageDigest> 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<MessageDigest> {
@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();
}
}
@@ -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
}
}
}
@@ -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;
@@ -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
@@ -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);
}
}
@@ -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<String> 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<CryptoFile> 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<CryptoFolder> 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
@@ -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() {
@@ -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;
@@ -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;
@@ -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());
}
}
@@ -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;
}
}
@@ -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<MessageDigest> 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<MessageDigest> {
@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;
}
}
}
@@ -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);
}
}
}
@@ -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();
}
}
@@ -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<? extends Folder> 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();
}
}
@@ -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<Node> visitor) {
walk(folder, 0, Integer.MAX_VALUE, visitor);
}
public static void walk(Folder folder, int depth, int maxDepth, Consumer<Node> 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);
}
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<Root level="DEBUG">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>
+1
View File
@@ -0,0 +1 @@
/target/
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2015 Sebastian Stenzel
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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.11.0-SNAPSHOT</version>
</parent>
<artifactId>filesystem-inmemory</artifactId>
<name>Cryptomator in-memory filesystem</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
</dependencies>
</project>
@@ -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);
}
@@ -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;
@@ -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<String, InMemoryNode> children = new TreeMap<>();
final Map<String, InMemoryNode> volatileChildren = new HashMap<>();
InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) {
public InMemoryFolder(InMemoryFolder parent, String name, Instant lastModified) {
super(parent, name, lastModified);
}
@@ -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;
@@ -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;
+15 -3
View File
@@ -56,12 +56,23 @@
<!-- modules -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>core</artifactId>
<artifactId>filesystem-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
<artifactId>filesystem-inmemory</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>crypto-layer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
@@ -198,11 +209,12 @@
<modules>
<module>filesystem-api</module>
<module>filesystem-inmemory</module>
<module>crypto-layer</module>
<module>crypto-api</module>
<module>crypto-aes</module>
<module>core</module>
<module>ui</module>
<module>crypto-layer</module>
</modules>
<profiles>