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
+4
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>
@@ -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;
}
}
@@ -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);
}
@@ -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
@@ -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)) {
@@ -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));
}
@@ -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));
@@ -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());
}
}
@@ -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
@@ -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());
}
}