mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-17 20:56:02 +00:00
began implementation of new filesystem api based encryption layer
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -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;
|
||||
+14
-5
@@ -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
|
||||
+25
-10
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+37
-23
@@ -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
|
||||
+13
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+57
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
/target/
|
||||
@@ -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>
|
||||
+10
-2
@@ -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);
|
||||
}
|
||||
|
||||
+8
@@ -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;
|
||||
+10
-2
@@ -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);
|
||||
}
|
||||
|
||||
+10
-2
@@ -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;
|
||||
+8
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user