added new "shortening layer" responsible for shortening long file names

the crypto layer is no longer resposible for the postprocessing of long names, as this is an unrelated task without any security implications
This commit is contained in:
Sebastian Stenzel
2015-12-16 18:37:08 +01:00
parent b41ccb6054
commit eadf736e98
20 changed files with 675 additions and 45 deletions

View File

@@ -27,6 +27,10 @@
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>shortening-layer</artifactId>
</dependency>
<!-- Crypto -->
<dependency>

View File

@@ -46,17 +46,18 @@ public class CryptorImpl implements Cryptor {
@Override
public FilenameCryptor getFilenameCryptor() {
// lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509
FilenameCryptor cryptor = filenameCryptor.get();
if (cryptor == null) {
cryptor = new FilenameCryptorImpl(encryptionKey, macKey);
if (filenameCryptor.compareAndSet(null, cryptor)) {
return cryptor;
final FilenameCryptor existingCryptor = filenameCryptor.get();
if (existingCryptor != null) {
return existingCryptor;
} else {
final FilenameCryptorImpl newCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
if (filenameCryptor.compareAndSet(null, newCryptor)) {
return newCryptor;
} else {
// CAS failed: other thread set an object
newCryptor.destroy();
return filenameCryptor.get();
}
} else {
return cryptor;
}
}

View File

@@ -15,7 +15,6 @@ 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;
@@ -26,7 +25,7 @@ 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 ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final SivMode AES_SIV = new SivMode();
private final SecretKey encryptionKey;
@@ -44,7 +43,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
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);
final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
return BASE32.encodeAsString(hashedBytes);
}
@@ -66,14 +65,14 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
}
private static class ThreadLocalSha256 extends ThreadLocal<MessageDigest> {
private static class ThreadLocalSha1 extends ThreadLocal<MessageDigest> {
@Override
protected MessageDigest initialValue() {
try {
return MessageDigest.getInstance("SHA-256");
return MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("SHA-256 exists in every JVM");
throw new AssertionError("SHA-1 exists in every JVM");
}
}
@@ -88,7 +87,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
/* ======================= destruction ======================= */
@Override
public void destroy() throws DestroyFailedException {
public void destroy() {
TheDestroyer.destroyQuietly(encryptionKey);
TheDestroyer.destroyQuietly(macKey);
}

View File

@@ -26,9 +26,8 @@ public class CryptoFile extends CryptoNode implements File {
super(parent, name, cryptor);
}
@Override
String encryptedName() {
return name() + FILE_EXT;
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
}
@Override

View File

@@ -29,7 +29,6 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystem.class);
private static final String DATA_ROOT_DIR = "d";
private static final String METADATA_ROOT_DIR = "m";
private static final String ROOT_DIR_FILE = "root";
private static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
@@ -96,11 +95,6 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
return physicalRoot.folder(DATA_ROOT_DIR);
}
@Override
Folder physicalMetadataRoot() {
return physicalRoot.folder(METADATA_ROOT_DIR);
}
@Override
public Optional<CryptoFolder> parent() {
return Optional.empty();
@@ -119,7 +113,6 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
@Override
public void create(FolderCreateMode mode) {
physicalDataRoot().create(mode);
physicalMetadataRoot().create(mode);
final File dirFile = physicalFile();
final String directoryId = getDirectoryId();
try (WritableFile writable = dirFile.openWritable(1, TimeUnit.SECONDS)) {

View File

@@ -12,8 +12,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -21,7 +19,6 @@ import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
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;
@@ -41,9 +38,8 @@ class CryptoFolder extends CryptoNode implements Folder {
super(parent, name, cryptor);
}
@Override
String encryptedName() {
return name() + FILE_EXT;
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
}
protected String getDirectoryId() {
@@ -72,14 +68,7 @@ class CryptoFolder extends CryptoNode implements Folder {
}
Folder physicalFolder() {
final String encryptedThenHashedDirId;
try {
final byte[] hash = MessageDigest.getInstance("SHA-1").digest(getDirectoryId().getBytes());
encryptedThenHashedDirId = new Base32().encodeAsString(hash);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("SHA-1 exists in every JVM");
}
// TODO actual encryption
final String encryptedThenHashedDirId = cryptor.getFilenameCryptor().hashDirectoryId(getDirectoryId());
return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
}

View File

@@ -30,10 +30,6 @@ abstract class CryptoNode implements Node {
return parent.physicalDataRoot();
}
Folder physicalMetadataRoot() {
return parent.physicalMetadataRoot();
}
@Override
public Optional<CryptoFolder> parent() {
return Optional.of(parent);
@@ -44,10 +40,6 @@ abstract class CryptoNode implements Node {
return name;
}
String encryptedName() {
return name();
}
@Override
public boolean exists() {
return parent.children().anyMatch(node -> node.equals(this));

View File

@@ -11,8 +11,10 @@ package org.cryptomator.crypto.engine.impl;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicReference;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.FilenameCryptor;
import org.junit.Assert;
import org.junit.Test;
@@ -50,4 +52,46 @@ public class CryptorImplTest {
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
}
@Test
public void testGetFilenameCryptorAfterUnlocking() {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
Assert.assertNotNull(cryptor.getFilenameCryptor());
}
@Test(expected = RuntimeException.class)
public void testGetFilenameCryptorBeforeUnlocking() {
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
cryptor.getFilenameCryptor();
}
@Test
public void testConcurrentGetFilenameCryptor() throws InterruptedException {
final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}";
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
final AtomicReference<FilenameCryptor> receivedByT1 = new AtomicReference<>();
final Thread t1 = new Thread(() -> {
receivedByT1.set(cryptor.getFilenameCryptor());
});
final AtomicReference<FilenameCryptor> receivedByT2 = new AtomicReference<>();
final Thread t2 = new Thread(() -> {
receivedByT2.set(cryptor.getFilenameCryptor());
});
t1.start();
t2.start();
t1.join();
t2.join();
// It is not guaranteed, both threads will enter getFilenameCryptor() exactly simultaneously. (But logging shows it is very likely)
// In any case both threads should receive the same FilenameCryptor
Assert.assertSame(receivedByT1.get(), receivedByT2.get());
}
}

View File

@@ -49,7 +49,7 @@ public class CryptoFileSystemTest {
Assert.assertTrue(masterkeyBkupFile.exists());
fs.create(FolderCreateMode.INCLUDING_PARENTS);
Assert.assertTrue(physicalDataRoot.exists());
Assert.assertEquals(4, physicalFs.children().count()); // d + m + masterkey.cryptomator + masterkey.cryptomator.bkup
Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup
Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file
Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory

View File

@@ -0,0 +1,62 @@
package org.cryptomator.crypto.fs;
import java.security.SecureRandom;
import java.util.Arrays;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.impl.CryptorImpl;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.Node;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.cryptomator.shortening.ShorteningFileSystem;
import org.junit.Assert;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EncryptAndShortenIntegrationTest {
private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class);
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
private static final long serialVersionUID = 1505563778398085504L;
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
@Test
public void testEncryptionOfLongFolderNames() {
final FileSystem physicalFs = new InMemoryFileSystem();
final FileSystem shorteningFs = new ShorteningFileSystem(physicalFs, physicalFs.folder("m"), 70);
final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK);
cryptor.randomizeMasterkey();
final FileSystem fs = new CryptoFileSystem(shorteningFs, cryptor, "foo");
fs.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
final Folder shortFolder = fs.folder("normal folder name");
shortFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
final Folder longFolder = fs.folder("this will be a long filename after encryption");
longFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
// the long name will produce a metadata file on the physical layer:
LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs));
Assert.assertEquals(1, physicalFs.folder("m").folders().count());
// on the second layer all .lng files are resolved to their actual names:
LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs));
DirectoryWalker.walk(shorteningFs, node -> {
Assert.assertFalse(node.name().endsWith(".lng"));
});
// on the third (cleartext layer) we have cleartext names on the root level:
LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs));
Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray());
}
}

View File

@@ -64,6 +64,11 @@
<artifactId>filesystem-inmemory</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>shortening-layer</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>crypto-layer</artifactId>
@@ -215,6 +220,7 @@
<module>crypto-aes</module>
<module>core</module>
<module>ui</module>
<module>shortening-layer</module>
</modules>
<profiles>

1
main/shortening-layer/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,58 @@
<?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>shortening-layer</artifactId>
<name>Cryptomator name shortening filesystem layer</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
</dependency>
<!-- Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-inmemory</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,111 @@
package org.cryptomator.shortening;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
class FilenameShortener {
private static final String LONG_NAME_FILE_EXT = ".lng";
private static final ThreadLocal<MessageDigest> SHA1 = new ThreadLocalSha1();
private static final BaseNCodec BASE32 = new Base32();
private final Folder metadataRoot;
private final int threshold;
public FilenameShortener(Folder metadataRoot, int threshold) {
this.metadataRoot = metadataRoot;
this.threshold = threshold;
}
public String inflate(String shortName) {
if (shortName.endsWith(LONG_NAME_FILE_EXT)) {
return loadMapping(shortName);
} else {
return shortName;
}
}
public String deflate(String longName) {
if (longName.length() < threshold) {
return longName;
} else {
final byte[] hashBytes = SHA1.get().digest(longName.getBytes());
final String hash = BASE32.encodeAsString(hashBytes);
return hash + LONG_NAME_FILE_EXT;
}
}
public boolean isShortened(String name) {
return name.endsWith(LONG_NAME_FILE_EXT);
}
public void saveMapping(String longName, String shortName) {
final File mappingFile = mappingFile(shortName);
if (!mappingFile.exists()) {
mappingFile.parent().get().create(FolderCreateMode.INCLUDING_PARENTS);
try (WritableFile writable = mappingFile.openWritable(1, TimeUnit.SECONDS)) {
writable.write(ByteBuffer.wrap(longName.getBytes(StandardCharsets.UTF_8)));
} catch (TimeoutException e) {
throw new UncheckedIOException(new IOException("Failed to lock mapping file in time. " + mappingFile, e));
}
}
}
private File mappingFile(String deflated) {
final Folder folder = metadataRoot.folder(deflated.substring(0, 2)).folder(deflated.substring(2, 4));
return folder.file(deflated);
}
private String loadMapping(String shortName) {
final File mappingFile = mappingFile(shortName);
if (!mappingFile.exists()) {
throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
} else {
try (ReadableFile readable = mappingFile.openReadable(1, TimeUnit.SECONDS)) {
// TODO buffer might be to small
final ByteBuffer buf = ByteBuffer.allocate(1024);
readable.read(buf);
buf.flip();
final byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
return new String(bytes, StandardCharsets.UTF_8);
} catch (TimeoutException e) {
throw new UncheckedIOException(new IOException("Failed to lock mapping file in time. " + mappingFile, e));
}
}
}
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 messageDigest = super.get();
messageDigest.reset();
return messageDigest;
}
}
}

View File

@@ -0,0 +1,38 @@
package org.cryptomator.shortening;
import java.io.UncheckedIOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
class ShorteningFile extends ShorteningNode<File>implements File {
private final FilenameShortener shortener;
public ShorteningFile(ShorteningFolder parent, File delegate, String longName, FilenameShortener shortener) {
super(parent, delegate, longName);
this.shortener = shortener;
}
@Override
public ReadableFile openReadable(long timeout, TimeUnit unit) throws UncheckedIOException, TimeoutException {
return delegate.openReadable(timeout, unit);
}
@Override
public WritableFile openWritable(long timeout, TimeUnit unit) throws UncheckedIOException, TimeoutException {
if (shortener.isShortened(shortName())) {
shortener.saveMapping(name(), shortName());
}
return delegate.openWritable(timeout, unit);
}
@Override
public String toString() {
return name();
}
}

View File

@@ -0,0 +1,29 @@
package org.cryptomator.shortening;
import java.util.Optional;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
public class ShorteningFileSystem extends ShorteningFolder implements FileSystem {
public ShorteningFileSystem(Folder root, Folder metadataRoot, int threshold) {
super(null, root, "", metadataRoot, new FilenameShortener(metadataRoot, threshold));
}
@Override
public Optional<ShorteningFolder> parent() {
return Optional.empty();
}
@Override
public boolean exists() {
return true;
}
@Override
public void delete() {
// no-op.
}
}

View File

@@ -0,0 +1,115 @@
package org.cryptomator.shortening;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.Node;
class ShorteningFolder extends ShorteningNode<Folder>implements Folder {
private final Folder metadataRoot;
private final FilenameShortener shortener;
public ShorteningFolder(ShorteningFolder parent, Folder delegate, String longName, Folder metadataRoot, FilenameShortener shortener) {
super(parent, delegate, longName);
this.metadataRoot = metadataRoot;
this.shortener = shortener;
}
@Override
public Stream<? extends Node> children() {
return Stream.concat(this.files(), this.folders());
}
private ShorteningFile existingFile(File original) {
final String longName = shortener.inflate(original.name());
return new ShorteningFile(this, original, longName, shortener);
}
@Override
public File file(String name) {
final File original = delegate.file(shortener.deflate(name));
if (metadataRoot.equals(original)) { // comparing apples and oranges, but we don't know if the underlying fs distinguishes files and folders...
throw new UncheckedIOException("'" + name + "' is a reserved name.", new FileAlreadyExistsException(name));
}
return new ShorteningFile(this, original, name, shortener);
}
@Override
public Stream<? extends File> files() throws UncheckedIOException {
return delegate.files().map(this::existingFile);
}
private ShorteningFolder existingFolder(Folder original) {
final String longName = shortener.inflate(original.name());
return new ShorteningFolder(this, original, longName, metadataRoot, shortener);
}
@Override
public Folder folder(String name) {
final Folder original = delegate.folder(shortener.deflate(name));
if (metadataRoot.equals(original)) {
throw new UncheckedIOException("'" + name + "' is a reserved name.", new FileAlreadyExistsException(name));
}
return new ShorteningFolder(this, original, name, metadataRoot, shortener);
}
@Override
public Stream<? extends Folder> folders() {
// if metadataRoot is inside our filesystem, we must filter it out:
final Predicate<Node> equalsMetadataRoot = (Node node) -> metadataRoot.equals(node);
return delegate.folders().filter(equalsMetadataRoot.negate()).map(this::existingFolder);
}
@Override
public void create(FolderCreateMode mode) {
if (!parent().get().exists() && FolderCreateMode.FAIL_IF_PARENT_IS_MISSING.equals(mode)) {
throw new UncheckedIOException(new FileNotFoundException(parent().get().name()));
} else if (!parent().get().exists() && FolderCreateMode.INCLUDING_PARENTS.equals(mode)) {
parent().get().create(mode);
}
assert parent().get().exists();
if (shortener.isShortened(shortName())) {
shortener.saveMapping(name(), shortName());
}
delegate.create(mode);
}
@Override
public void delete() {
delegate.delete();
}
@Override
public void moveTo(Folder target) {
if (target instanceof ShorteningFolder) {
moveToInternal((ShorteningFolder) target);
} else {
throw new UnsupportedOperationException("Can not move ShorteningFolder to conventional folder.");
}
}
private void moveToInternal(ShorteningFolder target) {
if (this.isAncestorOf(target) || target.isAncestorOf(this)) {
throw new IllegalArgumentException("Can not move directories containing one another (src: " + this + ", dst: " + target + ")");
}
if (!target.exists()) {
target.create(FolderCreateMode.INCLUDING_PARENTS);
}
delegate.moveTo(target.delegate);
}
@Override
public String toString() {
return name() + "/";
}
}

View File

@@ -0,0 +1,68 @@
package org.cryptomator.shortening;
import java.time.Instant;
import java.util.Optional;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
class ShorteningNode<E extends Node> implements Node {
protected final E delegate;
private final ShorteningFolder parent;
private final String longName;
private final String shortName;
public ShorteningNode(ShorteningFolder parent, E delegate, String longName) {
this.delegate = delegate;
this.parent = parent;
this.shortName = delegate.name();
this.longName = longName;
}
@Override
public String name() {
return longName;
}
protected String shortName() {
return shortName;
}
@Override
public Optional<? extends Folder> parent() {
return Optional.ofNullable(parent);
}
@Override
public boolean exists() {
return delegate.exists();
}
@Override
public Instant lastModified() {
return delegate.lastModified();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((longName == null) ? 0 : longName.hashCode());
result = prime * result + ((parent == null) ? 0 : parent.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ShorteningNode) {
ShorteningNode<?> other = (ShorteningNode<?>) obj;
return this.getClass() == other.getClass() //
&& (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
&& (this.longName == null && other.longName == null || this.longName.equals(other.longName));
} else {
return false;
}
}
}

View File

@@ -0,0 +1,6 @@
/**
* Provides a decoration layer for the {@link org.cryptomator.filesystem Filesystem API}.
* {@link org.cryptomator.filesystem.File File} and {@link org.cryptomator.filesystem.Folder Folder} names exceeding a certain length limit will be mapped to shorter equivalents.
* The mapping itself is stored in metadata files inside the <code>m/</code> directory on root level.
*/
package org.cryptomator.shortening;

View File

@@ -0,0 +1,115 @@
package org.cryptomator.shortening;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Test;
public class ShorteningFileSystemTest {
@Test
public void testCreationOfInvisibleMetadataFolder() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
fs.folder("morethantenchars").create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
Assert.assertTrue(metadataRoot.exists());
Assert.assertEquals(1, fs.folders().count());
}
@Test(expected = UncheckedIOException.class)
public void testPreventCreationOfMetadataFolder() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
fs.folder("m");
}
@Test
public void testDeflate() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
final Folder longNamedFolder = fs.folder("morethantenchars"); // base32(sha1(morethantenchars)) = QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP
final File correspondingMetadataFile = metadataRoot.folder("QM").folder("JL").file("QMJL5GQUETRX2YRV6XDTJQ6NNM7IEUHP.lng");
longNamedFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
Assert.assertTrue(longNamedFolder.exists());
Assert.assertTrue(correspondingMetadataFile.exists());
}
@Test
public void testDeflateAndInflateFolder() {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
final FileSystem fs1 = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
final Folder longNamedFolder1 = fs1.folder("morethantenchars");
longNamedFolder1.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
final FileSystem fs2 = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
final Folder longNamedFolder2 = fs2.folder("morethantenchars");
Assert.assertTrue(longNamedFolder2.exists());
}
@Test
public void testDeflateAndInflateFolderAndFile() throws UncheckedIOException, TimeoutException {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
// write:
final FileSystem fs1 = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
fs1.folder("morethantenchars").create(FolderCreateMode.INCLUDING_PARENTS);
try (WritableFile file = fs1.folder("morethantenchars").file("morethanelevenchars.txt").openWritable(1, TimeUnit.MILLISECONDS)) {
file.write(ByteBuffer.wrap("hello world".getBytes()));
}
// read
final FileSystem fs2 = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
try (ReadableFile file = fs2.folder("morethantenchars").file("morethanelevenchars.txt").openReadable(1, TimeUnit.MILLISECONDS)) {
ByteBuffer buf = ByteBuffer.allocate(11);
file.read(buf);
Assert.assertEquals("hello world", new String(buf.array()));
}
}
@Test
public void testPassthroughShortNamedFiles() throws UncheckedIOException, TimeoutException {
final FileSystem underlyingFs = new InMemoryFileSystem();
final Folder metadataRoot = underlyingFs.folder("m");
final FileSystem fs = new ShorteningFileSystem(underlyingFs, metadataRoot, 10);
// of folders:
underlyingFs.folder("foo").folder("bar").create(FolderCreateMode.INCLUDING_PARENTS);
Assert.assertTrue(fs.folder("foo").folder("bar").exists());
// from underlying:
try (WritableFile file = underlyingFs.folder("foo").file("test1.txt").openWritable(1, TimeUnit.MILLISECONDS)) {
file.write(ByteBuffer.wrap("hello world".getBytes()));
}
try (ReadableFile file = fs.folder("foo").file("test1.txt").openReadable(1, TimeUnit.MILLISECONDS)) {
ByteBuffer buf = ByteBuffer.allocate(11);
file.read(buf);
Assert.assertEquals("hello world", new String(buf.array()));
}
// to underlying:
try (WritableFile file = fs.folder("foo").file("test2.txt").openWritable(1, TimeUnit.MILLISECONDS)) {
file.write(ByteBuffer.wrap("hello world".getBytes()));
}
try (ReadableFile file = underlyingFs.folder("foo").file("test2.txt").openReadable(1, TimeUnit.MILLISECONDS)) {
ByteBuffer buf = ByteBuffer.allocate(11);
file.read(buf);
Assert.assertEquals("hello world", new String(buf.array()));
}
}
}