Permalink
Browse files

- Fixes #128 and #119 by using unique directory id as associated data…

… during filename encryption/decryption

- Using WeakValuedCache in all filesystem layers to prevent "twin" instances of the same folder
- Merge branch 'layered-io' of https://github.com/cryptomator/cryptomator into layered-io
  • Loading branch information...
overheadhunter committed Jan 10, 2016
2 parents 560c625 + f735a64 commit 3b178030c7a6001c1d070ee181aaae71f760d33f
Showing with 467 additions and 66 deletions.
  1. +9 −6 main/filesystem-api/src/main/java/org/cryptomator/filesystem/delegating/DelegatingFolder.java
  2. +13 −0 main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/DelegatingFolderTest.java
  3. +2 −2 main/filesystem-api/src/test/java/org/cryptomator/filesystem/delegating/TestDelegatingFolder.java
  4. +9 −4 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
  5. +2 −2 main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedFolder.java
  6. +4 −1 main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java
  7. +24 −9 main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java
  8. +2 −1 main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoNode.java
  9. +12 −0 main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java
  10. +3 −4 main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFile.java
  11. +22 −28 main/filesystem-inmemory/src/main/java/org/cryptomator/filesystem/inmem/InMemoryFolder.java
  12. +4 −4 ...stem-nameshortening/src/main/java/org/cryptomator/filesystem/blacklisting/BlacklistingFolder.java
  13. +2 −2 ...lesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/ShorteningFolder.java
  14. +2 −2 ...krabbit-filesystem-adapter/src/main/java/org/cryptomator/filesystem/jackrabbit/FolderLocator.java
  15. +174 −0 ...jackrabbit-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/LoggingHttpFilter.java
  16. +27 −0 ...-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletRequest.java
  17. +27 −0 ...filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingHttpServletResponse.java
  18. +73 −0 ...-filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletInputStream.java
  19. +54 −0 ...filesystem-adapter/src/test/java/org/cryptomator/webdav/filters/RecordingServletOutputStream.java
  20. +2 −0 ...m-adapter/src/test/java/org/cryptomator/webdav/jackrabbitservlet/FileSystemBasedWebDavServer.java
  21. +0 −1 main/jackrabbit-filesystem-adapter/src/test/resources/log4j2.xml
@@ -13,6 +13,7 @@
import java.util.Optional;
import java.util.stream.Stream;
import org.cryptomator.common.WeakValuedCache;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
@@ -21,6 +22,8 @@
implements Folder {
private final D parent;
private final WeakValuedCache<Folder, D> folders = WeakValuedCache.usingLoader(this::newFolder);
private final WeakValuedCache<File, F> files = WeakValuedCache.usingLoader(this::newFile);
public DelegatingFolder(D parent, Folder delegate) {
super(delegate);
@@ -39,27 +42,27 @@ public DelegatingFolder(D parent, Folder delegate) {
@Override
public Stream<D> folders() {
return delegate.folders().map(this::folder);
return delegate.folders().map(folders::get);
}
@Override
public Stream<F> files() throws UncheckedIOException {
return delegate.files().map(this::file);
return delegate.files().map(files::get);
}
@Override
public F file(String name) throws UncheckedIOException {
return file(delegate.file(name));
return files.get(delegate.file(name));
}
protected abstract F file(File delegate);
protected abstract F newFile(File delegate);
@Override
public D folder(String name) throws UncheckedIOException {
return folder(delegate.folder(name));
return folders.get(delegate.folder(name));
}
protected abstract D folder(Folder delegate);
protected abstract D newFolder(Folder delegate);
@Override
public void create() throws UncheckedIOException {
@@ -161,4 +161,17 @@ public void testDelete() {
Mockito.verify(mockFolder).delete();
}
@Test
public void testSubresourcesAreSameInstance() {
Folder mockFolder = Mockito.mock(Folder.class);
Folder mockSubFolder = Mockito.mock(Folder.class);
File mockSubFile = Mockito.mock(File.class);
Mockito.when(mockFolder.folder("mockSubFolder")).thenReturn(mockSubFolder);
Mockito.when(mockFolder.file("mockSubFile")).thenReturn(mockSubFile);
DelegatingFolder<?, ?, ?, ?> delegatingFolder = new TestDelegatingFolder(null, mockFolder);
Assert.assertSame(delegatingFolder.folder("mockSubFolder"), delegatingFolder.folder("mockSubFolder"));
Assert.assertSame(delegatingFolder.file("mockSubFile"), delegatingFolder.file("mockSubFile"));
}
}
@@ -10,12 +10,12 @@ public TestDelegatingFolder(TestDelegatingFolder parent, Folder delegate) {
}
@Override
protected TestDelegatingFile file(File delegate) {
protected TestDelegatingFile newFile(File delegate) {
return new TestDelegatingFile(this, delegate);
}
@Override
protected TestDelegatingFolder folder(Folder delegate) {
protected TestDelegatingFolder newFolder(Folder delegate) {
return new TestDelegatingFolder(this, delegate);
}
@@ -26,7 +26,12 @@
private static final BaseNCodec BASE32 = new Base32();
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final SivMode AES_SIV = new SivMode();
private static final ThreadLocal<SivMode> AES_SIV = new ThreadLocal<SivMode>() {
@Override
protected SivMode initialValue() {
return new SivMode();
};
};
private final SecretKey encryptionKey;
private final SecretKey macKey;
@@ -39,23 +44,23 @@
@Override
public String hashDirectoryId(String cleartextDirectoryId) {
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes);
final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
return BASE32.encodeAsString(hashedBytes);
}
@Override
public String encryptFilename(String cleartextName, byte[]... associatedData) {
final byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
final byte[] encryptedBytes = AES_SIV.get().encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
return BASE32.encodeAsString(encryptedBytes);
}
@Override
public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
final byte[] encryptedBytes = BASE32.decode(ciphertextName);
try {
final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
final byte[] cleartextBytes = AES_SIV.get().decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
return new String(cleartextBytes, UTF_8);
} catch (AEADBadTagException e) {
throw new AuthenticationFailedException("Authentication failed.", e);
@@ -22,12 +22,12 @@ public BlockAlignedFolder(BlockAlignedFolder parent, Folder delegate, int blockS
}
@Override
protected BlockAlignedFile file(File delegate) {
protected BlockAlignedFile newFile(File delegate) {
return new BlockAlignedFile(this, delegate, blockSize);
}
@Override
protected BlockAlignedFolder folder(Folder delegate) {
protected BlockAlignedFolder newFolder(Folder delegate) {
return new BlockAlignedFolder(this, delegate, blockSize);
}
@@ -8,6 +8,8 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.UncheckedIOException;
import java.time.Instant;
import java.util.Optional;
@@ -27,7 +29,8 @@ public CryptoFile(CryptoFolder parent, String name, Cryptor cryptor) {
@Override
protected String encryptedName() {
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
final byte[] parentDirId = parent.getDirectoryId().getBytes(UTF_8);
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + FILE_EXT;
}
@Override
@@ -23,6 +23,7 @@
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.common.WeakValuedCache;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
@@ -31,8 +32,10 @@
class CryptoFolder extends CryptoNode implements Folder {
static final String FILE_EXT = ".dir";
static final String DIR_EXT = ".dir";
private final WeakValuedCache<String, CryptoFolder> folders = WeakValuedCache.usingLoader(this::newFolder);
private final WeakValuedCache<String, CryptoFile> files = WeakValuedCache.usingLoader(this::newFile);
private final AtomicReference<String> directoryId = new AtomicReference<>();
public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
@@ -41,7 +44,8 @@ public CryptoFolder(CryptoFolder parent, String name, Cryptor cryptor) {
@Override
protected String encryptedName() {
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
final byte[] parentDirId = parent().map(CryptoFolder::getDirectoryId).map(s -> s.getBytes(UTF_8)).orElse(null);
return cryptor.getFilenameCryptor().encryptFilename(name(), parentDirId) + DIR_EXT;
}
Folder physicalFolder() {
@@ -77,31 +81,41 @@ public Instant lastModified() {
@Override
public Stream<CryptoFile> files() {
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptFileName).map(this::file);
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFile.FILE_EXT)).map(this::decryptChildFileName).map(this::file);
}
private String decryptFileName(String encryptedFileName) {
private String decryptChildFileName(String encryptedFileName) {
final byte[] dirId = getDirectoryId().getBytes(UTF_8);
final String ciphertext = StringUtils.removeEnd(encryptedFileName, CryptoFile.FILE_EXT);
return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
}
@Override
public CryptoFile file(String name) {
return files.get(name);
}
public CryptoFile newFile(String name) {
return new CryptoFile(this, name, cryptor);
}
@Override
public Stream<CryptoFolder> folders() {
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.FILE_EXT)).map(this::decryptFolderName).map(this::folder);
return physicalFolder().files().map(File::name).filter(s -> s.endsWith(CryptoFolder.DIR_EXT)).map(this::decryptChildFolderName).map(this::folder);
}
private String decryptFolderName(String encryptedFolderName) {
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.FILE_EXT);
return cryptor.getFilenameCryptor().decryptFilename(ciphertext);
private String decryptChildFolderName(String encryptedFolderName) {
final byte[] dirId = getDirectoryId().getBytes(UTF_8);
final String ciphertext = StringUtils.removeEnd(encryptedFolderName, CryptoFolder.DIR_EXT);
return cryptor.getFilenameCryptor().decryptFilename(ciphertext, dirId);
}
@Override
public CryptoFolder folder(String name) {
return folders.get(name);
}
public CryptoFolder newFolder(String name) {
return new CryptoFolder(this, name, cryptor);
}
@@ -139,6 +153,7 @@ private void moveToInternal(CryptoFolder target) {
// directoryId is now used by target, we must no longer use the same id
// (we'll generate a new one when needed)
target.directoryId.set(getDirectoryId());
directoryId.set(null);
}
@@ -49,7 +49,8 @@ public String name() {
@Override
public boolean exists() {
return parent.children().anyMatch(node -> node.equals(this));
return physicalFile().exists();
// return parent.children().anyMatch(node -> node.equals(this));
}
@Override
@@ -77,6 +77,18 @@ public void testDecryptionOfManipulatedFilename() {
filenameCryptor.decryptFilename(new String(encrypted, UTF_8));
}
@Test
public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() {
final byte[] keyBytes = new byte[32];
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8));
final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8));
Assert.assertNotEquals(encrypted1, encrypted2);
}
@Test
public void testDeterministicEncryptionOfFilenamesWithAssociatedData() {
final byte[] keyBytes = new byte[32];
@@ -45,7 +45,7 @@ public WritableFile openWritable() {
final WriteLock writeLock = lock.writeLock();
writeLock.lock();
final InMemoryFolder parent = parent().get();
parent.children.compute(this.name(), (k, v) -> {
parent.existingChildren.compute(this.name(), (k, v) -> {
if (v == null || v == this) {
this.lastModified = Instant.now();
this.creationTime = Instant.now();
@@ -54,7 +54,6 @@ public WritableFile openWritable() {
throw new UncheckedIOException(new FileExistsException(k));
}
});
parent.volatileFiles.remove(name);
return new InMemoryWritableFile(this::setLastModified, this::setCreationTime, this::getContent, this::setContent, this::delete, writeLock);
}
@@ -76,11 +75,11 @@ private void setContent(ByteBuffer content) {
private void delete(Void param) {
final InMemoryFolder parent = parent().get();
parent.children.computeIfPresent(this.name(), (k, v) -> {
parent.existingChildren.computeIfPresent(this.name(), (k, v) -> {
// returning null removes the entry.
return null;
});
assert !this.exists();
assert!this.exists();
}
@Override
Oops, something went wrong.

0 comments on commit 3b17803

Please sign in to comment.