first tests with refactored io layers

This commit is contained in:
Sebastian Stenzel
2015-12-14 04:37:29 +01:00
parent 3971d3afd5
commit e1b74ce312
19 changed files with 859 additions and 13 deletions
@@ -0,0 +1,44 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.time.Instant;
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;
public class CryptoFile extends CryptoNode implements File {
private static final String ENCRYPTED_FILE_EXT = ".file";
public CryptoFile(CryptoFolder parent, String name) {
super(parent, name);
}
@Override
String encryptedName() {
return name() + ENCRYPTED_FILE_EXT;
}
@Override
public Instant lastModified() throws UncheckedIOException {
// TODO Auto-generated method stub
return null;
}
@Override
public ReadableFile openReadable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
// TODO Auto-generated method stub
return null;
}
@Override
public WritableFile openWritable(long timeout, TimeUnit unit) throws IOException, TimeoutException {
// TODO Auto-generated method stub
return null;
}
}
@@ -0,0 +1,74 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.util.Optional;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
public class CryptoFileSystem extends CryptoFolder implements FileSystem {
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_FILE = "masterkey.cryptomator";
private static final String MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
private final Folder physicalRoot;
public CryptoFileSystem(Folder physicalRoot) {
super(null, "");
this.physicalRoot = physicalRoot;
}
@Override
File physicalFile() throws IOException {
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);
}
@Override
Folder physicalMetadataRoot() {
return physicalRoot.folder(METADATA_ROOT_DIR);
}
@Override
public Optional<CryptoFolder> parent() {
return Optional.empty();
}
@Override
public boolean exists() {
return physicalRoot.exists();
}
@Override
public void delete() {
// no-op.
}
@Override
public String toString() {
return "/";
}
@Override
public void create(FolderCreateMode mode) throws IOException {
physicalDataRoot().create(mode);
physicalMetadataRoot().create(mode);
super.create(mode);
}
}
@@ -0,0 +1,146 @@
package org.cryptomator.crypto;
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;
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.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.Node;
import org.cryptomator.filesystem.ReadableFile;
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";
private final AtomicReference<String> directoryId = new AtomicReference<>();
public CryptoFolder(CryptoFolder parent, String name) {
super(parent, name);
}
@Override
String encryptedName() {
return name() + ENCRYPTED_DIR_EXT;
}
protected String getDirectoryId() throws IOException {
if (directoryId.get() == null) {
File dirFile = physicalFile();
if (dirFile.exists()) {
try (ReadableFile readable = dirFile.openReadable(1, TimeUnit.SECONDS)) {
final ByteBuffer buf = ByteBuffer.allocate(64);
readable.read(buf);
buf.flip();
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
directoryId.set(new String(bytes));
} catch (TimeoutException e) {
throw new IOException("Failed to lock directory file in time." + dirFile, e);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
directoryId.compareAndSet(null, UUID.randomUUID().toString());
}
}
return directoryId.get();
}
File physicalFile() throws IOException {
return parent.physicalFolder().file(encryptedName());
}
Folder physicalFolder() throws IOException {
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
return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
}
@Override
public Instant lastModified() {
try {
return physicalFile().lastModified();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public Stream<? extends Node> children() throws IOException {
return Stream.concat(files(), folders());
}
@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);
}
private String decryptFileName(String encryptedFileName) {
// TODO Auto-generated method stub
return StringUtils.removeEnd(encryptedFileName, ENCRYPTED_FILE_EXT);
}
@Override
public CryptoFile file(String name) {
return new CryptoFile(this, name);
}
@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);
}
private String decryptFolderName(String encryptedFolderName) {
// TODO Auto-generated method stub
return StringUtils.removeEnd(encryptedFolderName, ENCRYPTED_DIR_EXT);
}
@Override
public CryptoFolder folder(String name) {
return new CryptoFolder(this, name);
}
@Override
public void create(FolderCreateMode mode) throws IOException {
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);
}
}
@Override
public void delete() throws IOException {
// TODO Auto-generated method stub
}
}
@@ -0,0 +1,72 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Optional;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
abstract class CryptoNode implements Node {
protected final CryptoFolder parent;
protected final String name;
public CryptoNode(CryptoFolder parent, String name) {
this.parent = parent;
this.name = name;
}
Folder physicalDataRoot() {
return parent.physicalDataRoot();
}
Folder physicalMetadataRoot() {
return parent.physicalMetadataRoot();
}
@Override
public Optional<CryptoFolder> parent() {
return Optional.of(parent);
}
@Override
public String name() {
return name;
}
String encryptedName() {
return name();
}
@Override
public boolean exists() {
try {
return parent.children().anyMatch(node -> node.equals(this));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((parent == null) ? 0 : parent.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CryptoNode) {
CryptoNode other = (CryptoNode) obj;
return this.getClass() == other.getClass() //
&& (this.parent == null && other.parent == null || this.parent.equals(other.parent)) //
&& (this.name == null && other.name == null || this.name.equals(other.name));
} else {
return false;
}
}
}
@@ -0,0 +1,4 @@
/**
* 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;
@@ -0,0 +1,38 @@
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());
}
}