diff --git a/README.md b/README.md
index 6b9f2e1c..1f252552 100644
--- a/README.md
+++ b/README.md
@@ -5,27 +5,23 @@
[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptofs&utm_campaign=Badge_Coverage)
[![Coverity Scan Build Status](https://scan.coverity.com/projects/10006/badge.svg)](https://scan.coverity.com/projects/cryptomator-cryptofs)
-**CryptoFS** - Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme.
-
-## Disclaimer
-
-This project is in an early stage and not ready for production use. We recommend to use it only for testing and evaluation purposes.
+**CryptoFS:** Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme.
## Features
- Access Cryptomator encrypted vaults from within your Java application
-- Uses a ``java.nio.file.FileSystem`` so code written against the java.nio.file API can easily be adapted to work with encrypted data
+- Uses a `java.nio.file.FileSystem` so code written against the `java.nio.file` API can easily be adapted to work with encrypted data
- Open Source means: No backdoors, control is better than trust
### Security Architecture
-For more information on the security details visit [cryptomator.org](https://cryptomator.org/architecture/).
+For more information on the security details, visit [cryptomator.org](https://cryptomator.org/architecture/).
## Usage
-CryptoFS depends on a Java 8 JRE/JDK. In addition the JCE unlimited strength policy files (needed for 256-bit keys) must be installed.
+CryptoFS depends on Java 8 JRE/JDK. In addition, the JCE unlimited strength policy files (needed for 256-bit keys) must be installed.
-### Vault initialization
+### Vault Initialization
```java
Path storageLocation = Paths.get("/home/cryptobot/vault");
@@ -33,9 +29,9 @@ Files.createDirectories(storageLocation);
CryptoFileSystemProvider.initialize(storageLocation, "masterkey.cryptomator", "password");
```
-### Obtaining a FileSystem instance
+### Obtaining a FileSystem Instance
-You have the option to use the convenience method ``CryptoFileSystemProvider#newFileSystem`` as follows:
+You have the option to use the convenience method `CryptoFileSystemProvider#newFileSystem` as follows:
```java
FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(
@@ -46,7 +42,7 @@ FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(
.build());
```
-or to use one of the standard methods from ``FileSystems#newFileSystem``:
+or to use one of the standard methods from `FileSystems#newFileSystem`:
```java
URI uri = CryptoFileSystemUri.create(storageLocation);
@@ -58,11 +54,11 @@ FileSystem fileSystem = FileSystems.newFileSystem(
.build());
```
-**Note** - Instead of CryptoFileSystemProperties you can always pass in a ``java.util.Map`` with entries set accordingly.
+**Note:** Instead of `CryptoFileSystemProperties`, you can always pass in a `java.util.Map` with entries set accordingly.
-For more details on construction have a look at the javadoc of ``CryptoFileSytemProvider``, ``CryptoFileSytemProperties`` and ``CryptoFileSytemUris``.
+For more details on construction, have a look at the javadoc of `CryptoFileSytemProvider`, `CryptoFileSytemProperties`, and `CryptoFileSytemUris`.
-### Using the constructed file system
+### Using the Constructed FileSystem
```java
try (FileSystem fileSystem = ...) { // see above
@@ -84,7 +80,7 @@ try (FileSystem fileSystem = ...) { // see above
}
```
-For more details on how to use the constructed file system you may consult the [javadocs of the java.nio.file package](http://docs.oracle.com/javase/8/docs/api/java/nio/file/package-summary.html).
+For more details on how to use the constructed `FileSystem`, you may consult the [javadocs of the `java.nio.file` package](http://docs.oracle.com/javase/8/docs/api/java/nio/file/package-summary.html).
## Building
@@ -101,7 +97,7 @@ mvn clean install
## Contributing to CryptoFS
-Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md), if you would like to report a bug, ask a question or help us with coding.
+Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md) if you would like to report a bug, ask a question, or help us with coding.
## Code of Conduct
@@ -109,4 +105,4 @@ Help us keep Cryptomator open and inclusive. Please read and follow our [Code of
## License
-Distributed under the AGPLv3. See the `LICENSE.txt` file for more info.
+This project is dual-licensed under the AGPLv3 for FOSS projects as well as a commercial license derived from the LGPL for independent software vendors and resellers. If you want to use this library in applications that are *not* licensed under the AGPL, feel free to contact our [sales team](https://cryptomator.org/enterprise/).
diff --git a/pom.xml b/pom.xml
index 8bd458e6..f37d59a6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.cryptomator
cryptofs
- 1.4.2
+ 1.4.3
Cryptomator Crypto Filesystem
This library provides the Java filesystem provider used by Cryptomator.
https://github.com/cryptomator/cryptofs
@@ -15,9 +15,9 @@
1.8
- 1.1.6
- 2.11
- 23.0
+ 1.1.7
+ 2.13
+ 23.4-jre
1.7.25
UTF-8
@@ -102,7 +102,7 @@
org.mockito
mockito-core
- 2.8.47
+ 2.11.0
test
@@ -137,7 +137,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.6.2
+ 3.7.0
${java.version}
@@ -162,7 +162,7 @@
org.owasp
dependency-check-maven
- 2.1.0
+ 3.0.1
24
0
@@ -185,7 +185,7 @@
com.codacy
codacy-coverage-reporter
- 2.0.0
+ 2.0.1
assembly
@@ -276,7 +276,7 @@
maven-dependency-plugin
- 3.0.1
+ 3.0.2
generate-dependency-list
diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java
new file mode 100644
index 00000000..7022641a
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java
@@ -0,0 +1,118 @@
+package org.cryptomator.cryptofs;
+
+import static java.nio.file.FileVisitResult.CONTINUE;
+import static java.util.stream.Collectors.toSet;
+import static org.cryptomator.cryptofs.CiphertextDirectoryDeleter.DeleteResult.NO_FILES_DELETED;
+import static org.cryptomator.cryptofs.CiphertextDirectoryDeleter.DeleteResult.SOME_FILES_DELETED;
+
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Set;
+
+import javax.inject.Inject;
+
+@PerFileSystem
+class CiphertextDirectoryDeleter {
+
+ private final DirectoryStreamFactory directoryStreamFactory;
+
+ @Inject
+ public CiphertextDirectoryDeleter(DirectoryStreamFactory directoryStreamFactory) {
+ this.directoryStreamFactory = directoryStreamFactory;
+ }
+
+ public void deleteCiphertextDirIncludingNonCiphertextFiles(Path ciphertextDir, CryptoPath cleartextDir) throws IOException {
+ try {
+ Files.deleteIfExists(ciphertextDir);
+ } catch (DirectoryNotEmptyException e) {
+ switch (deleteNonCiphertextFiles(ciphertextDir, cleartextDir)) {
+ case NO_FILES_DELETED:
+ throw e;
+ case SOME_FILES_DELETED:
+ Files.delete(ciphertextDir);
+ break;
+ default:
+ throw new IllegalStateException("Unexpected enum constant");
+ }
+ }
+ }
+
+ private DeleteResult deleteNonCiphertextFiles(Path ciphertextDir, CryptoPath cleartextDir) throws IOException {
+ NonCiphertextFilesDeletingFileVisitor visitor;
+ try (CryptoDirectoryStream directoryStream = directoryStreamFactory.newDirectoryStream(cleartextDir, ignored -> true)) {
+ Set ciphertextFiles = directoryStream.ciphertextDirectoryListing().collect(toSet());
+ visitor = new NonCiphertextFilesDeletingFileVisitor(ciphertextFiles);
+ }
+ Files.walkFileTree(ciphertextDir, visitor);
+ return visitor.getNumDeleted() == 0 //
+ ? NO_FILES_DELETED //
+ : SOME_FILES_DELETED;
+ }
+
+ static enum DeleteResult {
+ NO_FILES_DELETED, SOME_FILES_DELETED
+ }
+
+ private static class NonCiphertextFilesDeletingFileVisitor implements FileVisitor {
+
+ private final Set ciphertextFiles;
+
+ private int numDeleted = 0;
+ private int level = 0;
+
+ public NonCiphertextFilesDeletingFileVisitor(Set ciphertextFiles) {
+ this.ciphertextFiles = ciphertextFiles;
+ }
+
+ public int getNumDeleted() {
+ return numDeleted;
+ }
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ level++;
+ return CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ if (!isOnRootLevel() || !isCiphertextFile(file)) {
+ Files.delete(file);
+ numDeleted++;
+ }
+ return CONTINUE;
+ }
+
+ private boolean isOnRootLevel() {
+ return level == 1;
+ }
+
+ private boolean isCiphertextFile(Path file) throws IOException {
+ return ciphertextFiles.contains(file);
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ throw exc;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (exc != null) {
+ throw exc;
+ }
+ level--;
+ if (level > 0) {
+ Files.delete(dir);
+ numDeleted++;
+ }
+ return CONTINUE;
+ }
+ };
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java
index c98ec06a..38b3651d 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java
@@ -16,9 +16,8 @@
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Objects;
+import java.util.Optional;
import java.util.function.Consumer;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -30,7 +29,6 @@
class CryptoDirectoryStream implements DirectoryStream {
- private static final Pattern BASE32_PATTERN = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class);
private final String directoryId;
@@ -43,11 +41,14 @@ class CryptoDirectoryStream implements DirectoryStream {
private final DirectoryStream.Filter super Path> filter;
private final Consumer onClose;
private final FinallyUtil finallyUtil;
+ private final EncryptedNamePattern encryptedNamePattern;
public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNameCryptor filenameCryptor, CryptoPathMapper cryptoPathMapper, LongFileNameProvider longFileNameProvider,
- ConflictResolver conflictResolver, DirectoryStream.Filter super Path> filter, Consumer onClose, FinallyUtil finallyUtil) throws IOException {
+ ConflictResolver conflictResolver, DirectoryStream.Filter super Path> filter, Consumer onClose, FinallyUtil finallyUtil, EncryptedNamePattern encryptedNamePattern)
+ throws IOException {
this.onClose = onClose;
this.finallyUtil = finallyUtil;
+ this.encryptedNamePattern = encryptedNamePattern;
this.directoryId = ciphertextDir.dirId;
this.ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, p -> true);
LOG.trace("OPEN {}", directoryId);
@@ -61,36 +62,65 @@ public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNam
@Override
public Iterator iterator() {
- Stream pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false);
- Stream resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull);
- Stream sanitized = resolved.filter(this::passesPlausibilityChecks);
- Stream inflated = sanitized.map(this::inflateIfNeeded).filter(Objects::nonNull);
- Stream decrypted = inflated.map(this::decrypt).filter(Objects::nonNull);
- Stream filtered = decrypted.filter(this::isAcceptableByFilter);
- return filtered.iterator();
+ return cleartextDirectoryListing().iterator();
}
- private Path resolveConflictingFileIfNeeded(Path potentiallyConflictingPath) {
+ private Stream cleartextDirectoryListing() {
+ return directoryListing() //
+ .map(ProcessedPaths::getCleartextPath) //
+ .filter(this::isAcceptableByFilter);
+ }
+
+ public Stream ciphertextDirectoryListing() {
+ return directoryListing().map(ProcessedPaths::getCiphertextPath);
+ }
+
+ public Stream directoryListing() {
+ Stream pathIter = StreamSupport.stream(ciphertextDirStream.spliterator(), false).map(ProcessedPaths::new);
+ Stream resolved = pathIter.map(this::resolveConflictingFileIfNeeded).filter(Objects::nonNull);
+ Stream inflated = resolved.map(this::inflateIfNeeded).filter(Objects::nonNull);
+ Stream decrypted = inflated.map(this::decrypt).filter(Objects::nonNull);
+ Stream sanitized = decrypted.filter(this::passesPlausibilityChecks);
+ return sanitized;
+ }
+
+ private ProcessedPaths resolveConflictingFileIfNeeded(ProcessedPaths paths) {
try {
- return conflictResolver.resolveConflictsIfNecessary(potentiallyConflictingPath, directoryId);
+ return paths.withCiphertextPath(conflictResolver.resolveConflictsIfNecessary(paths.getCiphertextPath(), directoryId));
} catch (IOException e) {
- LOG.warn("I/O exception while finding potentially conflicting file versions for {}.", potentiallyConflictingPath);
+ LOG.warn("I/O exception while finding potentially conflicting file versions for {}.", paths.getCiphertextPath());
return null;
}
}
- private Path inflateIfNeeded(Path ciphertextPath) {
- String fileName = ciphertextPath.getFileName().toString();
+ private ProcessedPaths inflateIfNeeded(ProcessedPaths paths) {
+ String fileName = paths.getCiphertextPath().getFileName().toString();
if (LongFileNameProvider.isDeflated(fileName)) {
try {
String longFileName = longFileNameProvider.inflate(fileName);
- return ciphertextPath.resolveSibling(longFileName);
+ return paths.withInflatedPath(paths.getCiphertextPath().resolveSibling(longFileName));
} catch (IOException e) {
- LOG.warn(ciphertextPath + " could not be inflated.");
+ LOG.warn(paths.getCiphertextPath() + " could not be inflated.");
return null;
}
} else {
- return ciphertextPath;
+ return paths.withInflatedPath(paths.getCiphertextPath());
+ }
+ }
+
+ private ProcessedPaths decrypt(ProcessedPaths paths) {
+ Optional ciphertextName = encryptedNamePattern.extractEncryptedNameFromStart(paths.getInflatedPath());
+ if (ciphertextName.isPresent()) {
+ String ciphertext = ciphertextName.get();
+ try {
+ String cleartext = filenameCryptor.decryptFilename(ciphertext, directoryId.getBytes(StandardCharsets.UTF_8));
+ return paths.withCleartextPath(cleartextDir.resolve(cleartext));
+ } catch (AuthenticationFailedException e) {
+ LOG.warn(paths.getInflatedPath() + " not decryptable due to an unauthentic ciphertext.");
+ return null;
+ }
+ } else {
+ return null;
}
}
@@ -100,12 +130,13 @@ private Path inflateIfNeeded(Path ciphertextPath) {
* @param ciphertextPath The path to check.
* @return true
if the file is an existing ciphertext or directory file.
*/
- private boolean passesPlausibilityChecks(Path ciphertextPath) {
- return !isBrokenDirectoryFile(ciphertextPath);
+ private boolean passesPlausibilityChecks(ProcessedPaths paths) {
+ return !isBrokenDirectoryFile(paths);
}
- private boolean isBrokenDirectoryFile(Path potentialDirectoryFile) {
- if (potentialDirectoryFile.getFileName().toString().startsWith(Constants.DIR_PREFIX)) {
+ private boolean isBrokenDirectoryFile(ProcessedPaths paths) {
+ Path potentialDirectoryFile = paths.getCiphertextPath();
+ if (paths.getInflatedPath().getFileName().toString().startsWith(Constants.DIR_PREFIX)) {
final Path dirPath;
try {
dirPath = cryptoPathMapper.resolveDirectory(potentialDirectoryFile).path;
@@ -121,23 +152,6 @@ private boolean isBrokenDirectoryFile(Path potentialDirectoryFile) {
return false;
}
- private Path decrypt(Path ciphertextPath) {
- String ciphertextFileName = ciphertextPath.getFileName().toString();
- Matcher m = BASE32_PATTERN.matcher(ciphertextFileName);
- if (m.find()) {
- String ciphertext = m.group(1);
- try {
- String cleartext = filenameCryptor.decryptFilename(ciphertext, directoryId.getBytes(StandardCharsets.UTF_8));
- return cleartextDir.resolve(cleartext);
- } catch (AuthenticationFailedException e) {
- LOG.warn(ciphertextPath + " not decryptable due to an unauthentic ciphertext.");
- return null;
- }
- } else {
- return null;
- }
- }
-
private boolean isAcceptableByFilter(Path path) {
try {
return filter.accept(path);
@@ -155,4 +169,46 @@ public void close() throws IOException {
() -> LOG.trace("CLOSE {}", directoryId));
}
+ private static class ProcessedPaths {
+
+ private final Path ciphertextPath;
+ private final Path inflatedPath;
+ private final Path cleartextPath;
+
+ public ProcessedPaths(Path ciphertextPath) {
+ this(ciphertextPath, null, null);
+ }
+
+ private ProcessedPaths(Path ciphertextPath, Path inflatedPath, Path cleartextPath) {
+ this.ciphertextPath = ciphertextPath;
+ this.inflatedPath = inflatedPath;
+ this.cleartextPath = cleartextPath;
+ }
+
+ public Path getCiphertextPath() {
+ return ciphertextPath;
+ }
+
+ public Path getInflatedPath() {
+ return inflatedPath;
+ }
+
+ public Path getCleartextPath() {
+ return cleartextPath;
+ }
+
+ public ProcessedPaths withCiphertextPath(Path ciphertextPath) {
+ return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath);
+ }
+
+ public ProcessedPaths withInflatedPath(Path inflatedPath) {
+ return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath);
+ }
+
+ public ProcessedPaths withCleartextPath(Path cleartextPath) {
+ return new ProcessedPaths(ciphertextPath, inflatedPath, cleartextPath);
+ }
+
+ }
+
}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index 5905c7aa..3c844601 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -85,6 +85,7 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
private final CryptoPathFactory cryptoPathFactory;
private final CryptoFileSystemStats stats;
private final FinallyUtil finallyUtil;
+ private final CiphertextDirectoryDeleter ciphertextDirDeleter;
private volatile boolean open = true;
@@ -92,7 +93,8 @@ class CryptoFileSystemImpl extends CryptoFileSystem {
public CryptoFileSystemImpl(@PathToVault Path pathToVault, CryptoFileSystemProperties properties, Cryptor cryptor, CryptoFileSystemProvider provider, CryptoFileSystems cryptoFileSystems, CryptoFileStore fileStore,
OpenCryptoFiles openCryptoFiles, CryptoPathMapper cryptoPathMapper, DirectoryIdProvider dirIdProvider, CryptoFileAttributeProvider fileAttributeProvider,
CryptoFileAttributeViewProvider fileAttributeViewProvider, PathMatcherFactory pathMatcherFactory, CryptoPathFactory cryptoPathFactory, CryptoFileSystemStats stats,
- RootDirectoryInitializer rootDirectoryInitializer, CryptoFileAttributeByNameProvider fileAttributeByNameProvider, DirectoryStreamFactory directoryStreamFactory, FinallyUtil finallyUtil) {
+ RootDirectoryInitializer rootDirectoryInitializer, CryptoFileAttributeByNameProvider fileAttributeByNameProvider, DirectoryStreamFactory directoryStreamFactory, FinallyUtil finallyUtil,
+ CiphertextDirectoryDeleter ciphertextDirDeleter) {
this.cryptor = cryptor;
this.provider = provider;
this.cryptoFileSystems = cryptoFileSystems;
@@ -108,6 +110,7 @@ public CryptoFileSystemImpl(@PathToVault Path pathToVault, CryptoFileSystemPrope
this.cryptoPathFactory = cryptoPathFactory;
this.stats = stats;
this.directoryStreamFactory = directoryStreamFactory;
+ this.ciphertextDirDeleter = ciphertextDirDeleter;
this.rootPath = cryptoPathFactory.rootFor(this);
this.emptyPath = cryptoPathFactory.emptyFor(this);
this.finallyUtil = finallyUtil;
@@ -348,7 +351,7 @@ void delete(CryptoPath cleartextPath) throws IOException {
Path ciphertextDir = cryptoPathMapper.getCiphertextDirPath(cleartextPath);
Path ciphertextDirFile = cryptoPathMapper.getCiphertextFilePath(cleartextPath, CiphertextFileType.DIRECTORY);
try {
- Files.delete(ciphertextDir);
+ ciphertextDirDeleter.deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDir, cleartextPath);
if (!Files.deleteIfExists(ciphertextDirFile)) {
// should not happen. Nevertheless this is a valid state, so who no big deal...
LOG.warn("Successfully deleted dir {}, but didn't find corresponding dir file {}", ciphertextDir, ciphertextDirFile);
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index d94df6f6..ea9595af 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -162,6 +162,8 @@ public static void initialize(Path pathToVault, String masterkeyFilename, byte[]
String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
Path rootDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(rootDirHash.substring(0, 2)).resolve(rootDirHash.substring(2));
Files.createDirectories(rootDirPath);
+ // create "m":
+ Files.createDirectory(pathToVault.resolve(Constants.METADATA_DIR_NAME));
}
assert containsVault(pathToVault, masterkeyFilename);
}
diff --git a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java b/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java
index e3f88c1b..254f0033 100644
--- a/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java
+++ b/src/main/java/org/cryptomator/cryptofs/DirectoryStreamFactory.java
@@ -2,7 +2,6 @@
import java.io.IOException;
import java.nio.file.ClosedFileSystemException;
-import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.util.concurrent.ConcurrentHashMap;
@@ -21,21 +20,24 @@ class DirectoryStreamFactory {
private final ConflictResolver conflictResolver;
private final CryptoPathMapper cryptoPathMapper;
private final FinallyUtil finallyUtil;
+ private final EncryptedNamePattern encryptedNamePattern;
private final ConcurrentMap streams = new ConcurrentHashMap<>();
private volatile boolean closed = false;
@Inject
- public DirectoryStreamFactory(Cryptor cryptor, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver, CryptoPathMapper cryptoPathMapper, FinallyUtil finallyUtil) {
+ public DirectoryStreamFactory(Cryptor cryptor, LongFileNameProvider longFileNameProvider, ConflictResolver conflictResolver, CryptoPathMapper cryptoPathMapper, FinallyUtil finallyUtil,
+ EncryptedNamePattern encryptedNamePattern) {
this.cryptor = cryptor;
this.longFileNameProvider = longFileNameProvider;
this.conflictResolver = conflictResolver;
this.cryptoPathMapper = cryptoPathMapper;
this.finallyUtil = finallyUtil;
+ this.encryptedNamePattern = encryptedNamePattern;
}
- public DirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter super Path> filter) throws IOException {
+ public CryptoDirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter super Path> filter) throws IOException {
Directory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
CryptoDirectoryStream stream = new CryptoDirectoryStream( //
ciphertextDir, //
@@ -46,7 +48,8 @@ public DirectoryStream newDirectoryStream(CryptoPath cleartextDir, Filter<
conflictResolver, //
filter, //
closed -> streams.remove(closed), //
- finallyUtil);
+ finallyUtil, //
+ encryptedNamePattern);
streams.put(stream, stream);
if (closed) {
stream.close();
diff --git a/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java
new file mode 100644
index 00000000..7d1a9334
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/EncryptedNamePattern.java
@@ -0,0 +1,40 @@
+package org.cryptomator.cryptofs;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+
+@PerProvider
+class EncryptedNamePattern {
+
+ private static final Pattern BASE32_PATTERN = Pattern.compile("0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
+ private static final Pattern BASE32_PATTERN_AT_START_OF_NAME = Pattern.compile("^0?(([A-Z2-7]{8})*[A-Z2-7=]{8})");
+
+ @Inject
+ public EncryptedNamePattern() {
+ }
+
+ public Optional extractEncryptedName(Path ciphertextFile) {
+ String name = ciphertextFile.getFileName().toString();
+ Matcher matcher = BASE32_PATTERN.matcher(name);
+ if (matcher.find(0)) {
+ return Optional.of(matcher.group(1));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ public Optional extractEncryptedNameFromStart(Path ciphertextFile) {
+ String name = ciphertextFile.getFileName().toString();
+ Matcher matcher = BASE32_PATTERN_AT_START_OF_NAME.matcher(name);
+ if (matcher.find(0)) {
+ return Optional.of(matcher.group(1));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java
index 809a5606..d1ffa13d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java
@@ -61,6 +61,7 @@ public static void setupClass() {
private LongFileNameProvider longFileNameProvider;
private ConflictResolver conflictResolver;
private FinallyUtil finallyUtil;
+ private EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern();
@Before
@SuppressWarnings("unchecked")
@@ -126,7 +127,7 @@ public void testDirListing() throws IOException {
Mockito.when(dirStream.spliterator()).thenReturn(ciphertextFileNames.stream().map(cleartextPath::resolve).spliterator());
try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new Directory("foo", ciphertextDirPath), cleartextPath, filenameCryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL,
- DO_NOTHING_ON_CLOSE, finallyUtil)) {
+ DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) {
Iterator iter = stream.iterator();
Assert.assertTrue(iter.hasNext());
Assert.assertEquals(cleartextPath.resolve("one"), iter.next());
@@ -149,7 +150,7 @@ public void testDirListingForEmptyDir() throws IOException {
Mockito.when(dirStream.spliterator()).thenReturn(Spliterators.emptySpliterator());
try (CryptoDirectoryStream stream = new CryptoDirectoryStream(new Directory("foo", ciphertextDirPath), cleartextPath, filenameCryptor, cryptoPathMapper, longFileNameProvider, conflictResolver, ACCEPT_ALL,
- DO_NOTHING_ON_CLOSE, finallyUtil)) {
+ DO_NOTHING_ON_CLOSE, finallyUtil, encryptedNamePattern)) {
Iterator iter = stream.iterator();
Assert.assertFalse(iter.hasNext());
iter.next();
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 5ca1173c..df7fd75d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -99,6 +99,7 @@ public class CryptoFileSystemImplTest {
private final RootDirectoryInitializer rootDirectoryInitializer = mock(RootDirectoryInitializer.class);
private final DirectoryStreamFactory directoryStreamFactory = mock(DirectoryStreamFactory.class);
private final FinallyUtil finallyUtil = mock(FinallyUtil.class);
+ private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class);
private final CryptoPath root = mock(CryptoPath.class);
private final CryptoPath empty = mock(CryptoPath.class);
@@ -111,7 +112,7 @@ public void setup() {
when(cryptoPathFactory.emptyFor(any())).thenReturn(empty);
inTest = new CryptoFileSystemImpl(pathToVault, properties, cryptor, provider, cryptoFileSystems, fileStore, openCryptoFiles, cryptoPathMapper, dirIdProvider, fileAttributeProvider, fileAttributeViewProvider,
- pathMatcherFactory, cryptoPathFactory, stats, rootDirectoryInitializer, fileAttributeByNameProvider, directoryStreamFactory, finallyUtil);
+ pathMatcherFactory, cryptoPathFactory, stats, rootDirectoryInitializer, fileAttributeByNameProvider, directoryStreamFactory, finallyUtil, ciphertextDirDeleter);
}
@Test
@@ -360,7 +361,7 @@ public void testDeleteExistingDirectory() throws IOException {
when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false);
inTest.delete(cleartextPath);
- verify(physicalFsProv).delete(ciphertextDirPath);
+ verify(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath);
verify(physicalFsProv).deleteIfExists(ciphertextDirFilePath);
verify(dirIdProvider).delete(ciphertextDirFilePath);
}
@@ -368,7 +369,7 @@ public void testDeleteExistingDirectory() throws IOException {
@Test
public void testDeleteNonExistingFileOrDir() throws IOException {
when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false);
- Mockito.doThrow(new NoSuchFileException("cleartext")).when(physicalFsProv).delete(ciphertextDirPath);
+ Mockito.doThrow(new NoSuchFileException("cleartext")).when(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath);
thrown.expect(NoSuchFileException.class);
inTest.delete(cleartextPath);
@@ -377,7 +378,7 @@ public void testDeleteNonExistingFileOrDir() throws IOException {
@Test
public void testDeleteNonEmptyDir() throws IOException {
when(physicalFsProv.deleteIfExists(ciphertextFilePath)).thenReturn(false);
- Mockito.doThrow(new DirectoryNotEmptyException("ciphertextDir")).when(physicalFsProv).delete(ciphertextDirPath);
+ Mockito.doThrow(new DirectoryNotEmptyException("ciphertextDir")).when(ciphertextDirDeleter).deleteCiphertextDirIncludingNonCiphertextFiles(ciphertextDirPath, cleartextPath);
thrown.expect(DirectoryNotEmptyException.class);
inTest.delete(cleartextPath);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 32e3805c..4ff60b26 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -223,6 +223,9 @@ public void testDosFileAttributes() throws IOException {
assertThat(Files.getAttribute(file, "dos:archive"), is(true));
assertThat(Files.getAttribute(file, "dos:readOnly"), is(true));
+ // set readOnly to false again to allow deletion
+ Files.setAttribute(file, "dos:readOnly", false);
+
MoreFiles.deleteRecursively(tmpPath, RecursiveDeleteOption.ALLOW_INSECURE);
}
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
new file mode 100644
index 00000000..32f9cc2a
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -0,0 +1,191 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sebastian Stenzel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the accompanying LICENSE.txt.
+ *
+ * Contributors:
+ * Sebastian Stenzel - initial API and implementation
+ *******************************************************************************/
+package org.cryptomator.cryptofs;
+
+import static java.nio.file.Files.walkFileTree;
+import static java.nio.file.StandardOpenOption.CREATE_NEW;
+import static org.cryptomator.cryptofs.Constants.NAME_SHORTENING_THRESHOLD;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
+import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+/**
+ * Regression tests https://github.com/cryptomator/cryptofs/issues/17.
+ */
+public class DeleteNonEmptyCiphertextDirectoryIntegrationTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private static Path tempDir;
+ private static Path pathToVault;
+ private static Path mDir;
+ private static FileSystem fileSystem;
+
+ @BeforeClass
+ public static void setupClass() throws IOException {
+ tempDir = Files.createTempDirectory("DNECDIT");
+ pathToVault = tempDir.resolve("vault");
+ mDir = pathToVault.resolve("m");
+ Files.createDirectory(pathToVault);
+ fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build());
+ }
+
+ @AfterClass
+ public static void teardownClass() throws IOException {
+ walkFileTree(tempDir, new DeletingFileVisitor());
+ }
+
+ @Test
+ public void testDeleteCiphertextDirectoryContainingNonCryptoFile() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/z");
+ Files.createDirectory(cleartextDirectory);
+
+ Path ciphertextDirectory = firstEmptyCiphertextDirectory();
+ createFile(ciphertextDirectory, "foo01234.txt", new byte[] {65});
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteCiphertextDirectoryContainingDirectories() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/a");
+ Files.createDirectory(cleartextDirectory);
+
+ Path ciphertextDirectory = firstEmptyCiphertextDirectory();
+ // ciphertextDir
+ // .. foo0123
+ // .... foobar
+ // ...... test.baz
+ // .... text.txt
+ // .... text.data
+ Path foo0123 = createFolder(ciphertextDirectory, "foo0123");
+ Path foobar = createFolder(foo0123, "foobar");
+ createFile(foo0123, "test.txt", new byte[] {65});
+ createFile(foo0123, "text.data", new byte[] {65});
+ createFile(foobar, "test.baz", new byte[] {65});
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/b");
+ Files.createDirectory(cleartextDirectory);
+
+ Path ciphertextDirectory = firstEmptyCiphertextDirectory();
+ createFile(ciphertextDirectory, "HHEZJURE.lng", new byte[] {65});
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/c");
+ Files.createDirectory(cleartextDirectory);
+
+ Path ciphertextDirectory = firstEmptyCiphertextDirectory();
+ createFile(ciphertextDirectory, "HHEZJURE.lng", new byte[] {65});
+ Path mSubdir = mDir.resolve("HH").resolve("EZ");
+ Files.createDirectories(mSubdir);
+ createFile(mSubdir, "HHEZJURE.lng", "0HHEZJUREHHEZJUREHHEZJURE".getBytes());
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteNonEmptyDir() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/d");
+ Files.createDirectory(cleartextDirectory);
+ createFile(cleartextDirectory, "test", new byte[] {65});
+
+ thrown.expect(DirectoryNotEmptyException.class);
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/e");
+ Files.createDirectory(cleartextDirectory);
+
+ // a
+ // .. LongNameaaa...
+ String name = "LongName" + IntStream.range(0, NAME_SHORTENING_THRESHOLD) //
+ .mapToObj(ignored -> "a") //
+ .collect(Collectors.joining());
+ createFolder(cleartextDirectory, name);
+
+ thrown.expect(DirectoryNotEmptyException.class);
+
+ Files.delete(cleartextDirectory);
+ }
+
+ @Test
+ public void testDeleteEmptyDir() throws IOException {
+ Path cleartextDirectory = fileSystem.getPath("/f");
+ Files.createDirectory(cleartextDirectory);
+
+ Files.delete(cleartextDirectory);
+ }
+
+ private Path firstEmptyCiphertextDirectory() throws IOException {
+ try (Stream allFilesInVaultDir = Files.walk(pathToVault)) {
+ return allFilesInVaultDir //
+ .filter(Files::isDirectory) //
+ .filter(this::isEmptyDirectory) //
+ .filter(this::isEncryptedDirectory) //
+ .findFirst() //
+ .get();
+ }
+ }
+
+ private boolean isEmptyDirectory(Path path) {
+ try (Stream files = Files.list(path)) {
+ return files.count() == 0;
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private boolean isEncryptedDirectory(Path pathInVault) {
+ Path relativePath = pathToVault.relativize(pathInVault);
+ String relativePathAsString = relativePath.toString().replace(File.separatorChar, '/');
+ return relativePathAsString.matches("d/[2-7A-Z]{2}/[2-7A-Z]{30}");
+ }
+
+ private Path createFolder(Path parent, String name) throws IOException {
+ Path result = parent.resolve(name);
+ Files.createDirectory(result);
+ return result;
+ }
+
+ private Path createFile(Path parent, String name, byte[] data) throws IOException {
+ Path result = parent.resolve(name);
+ Files.write(result, data, CREATE_NEW);
+ return result;
+ }
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java
index 6c93b3c2..cb72352b 100644
--- a/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DirectoryStreamFactoryTest.java
@@ -41,8 +41,9 @@ public class DirectoryStreamFactoryTest {
private final LongFileNameProvider longFileNameProvider = mock(LongFileNameProvider.class);
private final ConflictResolver conflictResolver = mock(ConflictResolver.class);
private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class);
+ private final EncryptedNamePattern encryptedNamePattern = new EncryptedNamePattern();
- private final DirectoryStreamFactory inTest = new DirectoryStreamFactory(cryptor, longFileNameProvider, conflictResolver, cryptoPathMapper, finallyUtil);
+ private final DirectoryStreamFactory inTest = new DirectoryStreamFactory(cryptor, longFileNameProvider, conflictResolver, cryptoPathMapper, finallyUtil, encryptedNamePattern);
@SuppressWarnings("unchecked")
diff --git a/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java
new file mode 100644
index 00000000..d92f2285
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/EncryptedNamePatternTest.java
@@ -0,0 +1,58 @@
+package org.cryptomator.cryptofs;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+import org.junit.Test;
+
+public class EncryptedNamePatternTest {
+
+ private static final String ENCRYPTED_NAME = "ALKDUEEH2445375AUZEJFEFA";
+ private static final Path PATH_WITHOUT_ENCRYPTED_NAME = Paths.get("foo.txt");
+ private static final Path PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX = Paths.get("foo" + ENCRYPTED_NAME + ".txt");
+ private static final Path PATH_WITH_ENCRYPTED_NAME_AND_SUFFIX = Paths.get(ENCRYPTED_NAME + ".txt");
+
+ private EncryptedNamePattern inTest = new EncryptedNamePattern();
+
+ @Test
+ public void testExtractEncryptedNameReturnsEmptyOptionalIfNoEncryptedNameIsPresent() {
+ Optional result = inTest.extractEncryptedName(PATH_WITHOUT_ENCRYPTED_NAME);
+
+ assertThat(result.isPresent(), is(false));
+ }
+
+ @Test
+ public void testExtractEncryptedNameReturnsEncryptedNameIfItIsIsPresent() {
+ Optional result = inTest.extractEncryptedName(PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX);
+
+ assertThat(result.isPresent(), is(true));
+ assertThat(result.get(), is(ENCRYPTED_NAME));
+ }
+
+ @Test
+ public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfNoEncryptedNameIsPresent() {
+ Optional result = inTest.extractEncryptedNameFromStart(PATH_WITHOUT_ENCRYPTED_NAME);
+
+ assertThat(result.isPresent(), is(false));
+ }
+
+ @Test
+ public void testExtractEncryptedNameFromStartReturnsEncryptedNameIfItIsPresent() {
+ Optional result = inTest.extractEncryptedName(PATH_WITH_ENCRYPTED_NAME_AND_SUFFIX);
+
+ assertThat(result.isPresent(), is(true));
+ assertThat(result.get(), is(ENCRYPTED_NAME));
+ }
+
+ @Test
+ public void testExtractEncryptedNameFromStartReturnsEmptyOptionalIfEncryptedNameIsPresentAfterStart() {
+ Optional result = inTest.extractEncryptedNameFromStart(PATH_WITH_ENCRYPTED_NAME_AND_PREFIX_AND_SUFFIX);
+
+ assertThat(result.isPresent(), is(false));
+ }
+
+}