diff --git a/.gitignore b/.gitignore index 5ddd9fc9..4568d7a7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ test-output/ out/ .idea_modules/ *.iws +*.iml diff --git a/.travis.yml b/.travis.yml index 67c89ce8..2e45416a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: java -sudo: required +sudo: false jdk: -- oraclejdk8 +- oraclejdk9 env: global: - secure: LT4nMSKCu4qL+jQ80BdBIOqFO3GCyAPoxNkskS0q0wvscDpEx2bvFZa9KF6/dQxubhWnfACqWxxPKfF3VadfZoKn1z01TaZ/rKHkA5GedZweFO0wBIvi/gDIcAxVX0oPOkIruugYY3iDOzOqTUuBM686cW1XWs0LEV7qPTb6KM02/IeckQs+P9SSiarqSKROlQ8dABGdyxJTXheHHphFu4mDiQsi1vtub6OoQclKLLuK2MJvFiyDeZDYYXAnjFNC/pcBUBjr5b886zPB6HLLGgvQKRLvzQudedz08ZlJdnt3k6u7HvLINbs00U60fnD/+4krQQN4EEx0Natv4L1SxFjYO4wFK2FTCKoBMkVfjINqiWzmb/yhoG33Sw9VGiYdcV45QbH32CX1oiATohV+79gfIID6p3UOL1SZuELR2XzRq70K4Kw2BXig99a0+LjYCv4ynnzetqyWVZIdhBQ1Srf/4GxUwF21Urn9TJCNr2F5BcbqGrMUMvXNjTI0WqQCTMqM+Ha9Rbe27GG7ZMtMUHd83YWP0GDiSIg2S0T0lNL2e9iQGXsGBiX3Bz+E3HEWhnE4l6XKYVgn3NXrlDjwc2B6GTGeImZXkrbFFJwQihUSujj5H6l/+5a7NxbyA1MvzNwjeTaHzdNdYovTq6ywydVtF/Kt5h7oA2KmUoajaFs= #CODACY_PROJECT_TOKEN @@ -10,20 +10,22 @@ env: - secure: aLCaSgLfFniEPpiEdvEyFLlak4cTspVE3uqeGFCncPFUyyxPZ0JW+DdyFpvO5DXBa3hr9oS240H6f1T6Eo/J4K5SaKq3gS3yIzbvs2BS5yoCE4hGIZ6yS/70sIhlxvFvujJDT9KPYo8J6KE0w5vqy8BHEMZYBMEDWWvswwrvlqdaoZqe6vaoV1TsOeWeW+2qFdYu5j1KVQgSwOgzMdQuqQVSsrdA1FH+AzmMMbk3QgdGGd1U4MnFStIeG5/YfqVHkUVpZayhYB9yFzUCObT9R5zZ1OtghdOrkX1FRTKbFPAmJouToeFrIqrZEMbaE9PWaajuUZeqL2hQD1s0A+wj65cXjgbQhz4v7Gr0J/+2AU+9N+IRAIguiq6rXtt1HeR3YbBwlxFdwmEgYJjqO7+XFZFNYbr3YPgqbqJbgXpoxeUTdPNdNwckBUEQTTN6fs89Og6wHLDgoZb5niypSKWd/SFCU81FAGQ0mxy41jJOdltxVtG1+UOfb/41PRAzS6br7CpNnaq379kKlMiyOXY4UCWuFgwW4KhlUx0dgidqi2etcYrDZhZbguHmfLRfumEQ8nCp8LJ5FwJWlkmJhE8hFTu3sGmVUl06Ly1gybMPazev8hYjyr3vaexmek7qeqDtNT6n8L96qZkResjFI2pJmzZaNIg2yK92ZS/5v073OgM= # MAVEN_OSSRH_PASSWORD - secure: JUBj12rP7sEsNp8R3o9Mjwl7HCYKQan58CZ1WDm7L2MteFRDBJr625P4fk4cQOTjR6fb3jiogCD7dqkHoGJQlvi2ReQJLXKBhy3Bqtnzksf4c1By9bLB5NYH6MQV1FFazzAjjPjGlQYJB+kUx+pQ8vxMJC5yzp6vpKWu3vTobEFNtuFAWZq7qM91VJBUc8z+k1awas9vqgDlqWxH+K05L2c+tpnnmqXk9/HN9Z4akkKB++oACenE+w+D9Snyiif9WyhVR3+ZcMtL44YAQDabl6LlrTdM4afNaPcwgOdFuxL7GCZyIMgMP26O8JquK4ZtE2b/ZMz5h0jtE+r6M0E+yVAN+kG7UviNd9kKItiRiokDsaYeNCWVPHndGBtw6GU5GFNJTDhEb/oeqzclI/PaFHXA4O/o4A9N+nrzb28xWaihAM/NymVOE1iNNQfyNqPb9fjsMntNOTDT/sLH3tTJ62nvVPpurUXVV0+jbSrE20xclOZ1xBbMztQwya4EB/0fS+raCiR5spXHTNHpmSp2ubKAuo6V4AUXQXk6LHxiiIvcYSRYVgnBeOcreuDYv7+09xeIejxVcSzh7Nz7aK/0Fkq42ALYs/cDOq0mJGwoJ4yC7vlMj6GQNwZtCP+bzdGM3+cjTcD2F997Hf75lEVeYXK2oNE3/ycSQjdYlc3wTl0= #COVERITY_SCAN_TOKEN install: -- sudo apt-get install haveged -- mvn dependency:go-offline -Pdependency-check -- mvn dependency:go-offline -Pcoverage -- mvn source:help javadoc:help dependency:go-offline -Prelease +- mvn source:help javadoc:help dependency:go-offline -Pdependency-check,coverage,release before_script: - "if [[ ${TRAVIS_BRANCH} == 'develop' && ${TRAVIS_PULL_REQUEST} == 'false' ]]; then mvn dependency-check:check -Pdependency-check; fi" script: - "mvn clean test jacoco:report verify -Pcoverage" after_success: -- "bash <(curl -s https://codecov.io/bash)" +- jdk_switcher use oraclejdk8 +- curl -o ~/codacy-coverage-reporter-assembly-latest.jar https://oss.sonatype.org/service/local/repositories/releases/content/com/codacy/codacy-coverage-reporter/2.0.1/codacy-coverage-reporter-2.0.1-assembly.jar +- $JAVA_HOME/bin/java -cp ~/codacy-coverage-reporter-assembly-latest.jar com.codacy.CodacyCoverageReporter -l Java -r target/site/jacoco/jacoco.xml cache: directories: - $HOME/.m2 addons: + apt: + packages: + - haveged coverity_scan: project: name: "cryptomator/cryptofs" @@ -33,6 +35,7 @@ addons: build_command: "mvn compile -DskipTests=true" branch_pattern: release.* before_deploy: +- jdk_switcher use oraclejdk9 - "if ! gpg --list-secret-keys 34C80F11; then gpg --import 34C80F11.gpg; fi" deploy: - provider: script # SNAPSHOTS diff --git a/README.md b/README.md index 1f252552..3f2f4f85 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/cryptomator/cryptofs.svg?branch=develop)](https://travis-ci.org/cryptomator/cryptofs) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs) [![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) +[![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptofs/badge.svg)](https://snyk.io/test/github/cryptomator/cryptofs) **CryptoFS:** Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme. @@ -17,6 +17,15 @@ For more information on the security details, visit [cryptomator.org](https://cryptomator.org/architecture/). +## Audits + +- [Version 1.4.0 audit by Cure53](https://cryptomator.org/audits/2017-11-27%20crypto%20cure53.pdf) + +| Finding | Comment | +|---|---| +| 1u1-22-001 | The GPG key is used exclusively for the Maven repositories, is designed for signing only and is protected by a 30-character generated password (alphabet size: 96 chars). It is iterated and salted (SHA1 with 20971520 iterations). An offline attack is also very unattractive. Apart from that, this finding has no influence on the Tresor apps[1](#footnote-tresor-apps). This was not known to Cure53 at the time of reporting. | +| 1u1-22-002 | This issue is related to [siv-mode](https://github.com/cryptomator/siv-mode/). | + ## Usage CryptoFS depends on Java 8 JRE/JDK. In addition, the JCE unlimited strength policy files (needed for 256-bit keys) must be installed. @@ -106,3 +115,7 @@ Help us keep Cryptomator open and inclusive. Please read and follow our [Code of ## License 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/). + +--- + +1 The Cure53 pentesting was performed during the development of the apps for 1&1 Mail & Media GmbH. diff --git a/pom.xml b/pom.xml index 65c17dc8..00d646b7 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 org.cryptomator cryptofs - 1.4.5 + 1.5.0 Cryptomator Crypto Filesystem This library provides the Java filesystem provider used by Cryptomator. https://github.com/cryptomator/cryptofs @@ -14,10 +14,10 @@ - 1.8 - 1.1.7 - 2.13 - 23.5-jre + 8 + 1.2.0 + 2.15 + 23.6-jre 1.7.25 UTF-8 @@ -85,6 +85,12 @@ dagger ${dagger.version} + + com.google.dagger + dagger-compiler + ${dagger.version} + true + @@ -135,13 +141,6 @@ ${java.version} ${java.version} true - - - com.google.dagger - dagger-compiler - ${dagger.version} - - @@ -174,20 +173,6 @@ coverage - - - com.codacy - codacy-coverage-reporter - 2.0.1 - assembly - - - * - * - - - - @@ -203,28 +188,6 @@ - - org.codehaus.mojo - exec-maven-plugin - 1.6.0 - - - verify - - java - - - com.codacy.CodacyCoverageReporter - - -l - Java - -r - ${project.build.directory}/site/jacoco/jacoco.xml - - - - - @@ -257,7 +220,7 @@ maven-javadoc-plugin - 2.10.4 + 3.0.0 attach-javadocs diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java index 8ca73a16..09d47c6b 100644 --- a/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java +++ b/src/main/java/org/cryptomator/cryptofs/CiphertextDirectoryDeleter.java @@ -1,20 +1,19 @@ package org.cryptomator.cryptofs; -import static com.google.common.io.MoreFiles.deleteRecursively; -import static com.google.common.io.RecursiveDeleteOption.ALLOW_INSECURE; -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.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.DosFileAttributeView; import java.util.Set; import javax.inject.Inject; +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; + @PerFileSystem class CiphertextDirectoryDeleter { @@ -27,12 +26,21 @@ public CiphertextDirectoryDeleter(DirectoryStreamFactory directoryStreamFactory) public void deleteCiphertextDirIncludingNonCiphertextFiles(Path ciphertextDir, CryptoPath cleartextDir) throws IOException { try { - Files.deleteIfExists(ciphertextDir); + DeletingFileVisitor.forceDeleteIfExists(ciphertextDir); } catch (DirectoryNotEmptyException e) { /* * The directory may not be empty due to two reasons: - * 1. - * 2. + * 1. The directory really contains some valid ciphertext files + * 2. The directory does only contain files which are no cyphertext files + * + * In the first case the exception must be rethrown. In the second case the non cyphertext files and the + * directory must be deleted. + * + * Because we do not know at this point what is true we try to delete all non ciphertext files. If no non + * ciphertext files were deleted, we know that case 1 is true. If we deleted non ciphertext files both, + * case 1 or 2 could be true, thus we then reattempt the delete of the directory. If delete fails now, we + * can be sure that case 1 was true. Otherwise the exception is directly thrown because we are sure that + * case 2 is true. */ switch (deleteNonCiphertextFiles(ciphertextDir, cleartextDir)) { case NO_FILES_DELETED: @@ -52,7 +60,7 @@ private DeleteResult deleteNonCiphertextFiles(Path ciphertextDir, CryptoPath cle try (DirectoryStream stream = Files.newDirectoryStream(ciphertextDir, p -> !ciphertextFiles.contains(p))) { for (Path path : stream) { result = SOME_FILES_DELETED; - deleteRecursively(path, ALLOW_INSECURE); + Files.walkFileTree(path, DeletingFileVisitor.INSTANCE); } } return result; @@ -64,7 +72,7 @@ private Set ciphertextFiles(CryptoPath cleartextDir) throws IOException { } } - static enum DeleteResult { + enum DeleteResult { NO_FILES_DELETED, SOME_FILES_DELETED } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java index 90c7b49f..e32ee229 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoDirectoryStream.java @@ -8,11 +8,9 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; - import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; +import java.nio.file.DirectoryIteratorException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -29,6 +27,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.cryptomator.cryptofs.Constants.SHORT_NAMES_MAX_LENGTH; + class CryptoDirectoryStream implements DirectoryStream { private static final Logger LOG = LoggerFactory.getLogger(CryptoDirectoryStream.class); @@ -52,7 +52,7 @@ public CryptoDirectoryStream(Directory ciphertextDir, Path cleartextDir, FileNam this.finallyUtil = finallyUtil; this.encryptedNamePattern = encryptedNamePattern; this.directoryId = ciphertextDir.dirId; - this.ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path, p -> true); + this.ciphertextDirStream = Files.newDirectoryStream(ciphertextDir.path); LOG.trace("OPEN {}", directoryId); this.cleartextDir = cleartextDir; this.filenameCryptor = filenameCryptor; @@ -134,7 +134,7 @@ private ProcessedPaths decrypt(ProcessedPaths paths) { /** * Checks if a given file belongs into this ciphertext dir. * - * @param ciphertextPath The path to check. + * @param paths The path to check. * @return true if the file is an existing ciphertext or directory file. */ private boolean passesPlausibilityChecks(ProcessedPaths paths) { @@ -169,7 +169,12 @@ private boolean isAcceptableByFilter(Path path) { try { return filter.accept(path); } catch (IOException e) { - throw new UncheckedIOException(e); + // as defined by DirectoryStream's contract: + // > If an I/O error is encountered when accessing the directory then it + // > causes the {@code Iterator}'s {@code hasNext} or {@code next} methods to + // > throw {@link DirectoryIteratorException} with the {@link IOException} as the + // > cause. + throw new DirectoryIteratorException(e); } } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java index 4a018964..7caa3bdb 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java @@ -8,8 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import static org.cryptomator.cryptofs.Constants.SEPARATOR; - import java.io.File; import java.io.IOException; import java.net.URI; @@ -25,6 +23,8 @@ import java.util.LinkedList; import java.util.List; +import static org.cryptomator.cryptofs.Constants.SEPARATOR; + class CryptoPath implements Path { private static final String CURRENT_DIR = "."; @@ -64,6 +64,11 @@ public CryptoFileSystemImpl getFileSystem() { return fileSystem; } + // visible for testing + List getElements() { + return elements; + } + @Override public boolean isAbsolute() { fileSystem.assertOpen(); diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java index f80fe697..413e3a2d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathFactory.java @@ -1,5 +1,13 @@ package org.cryptomator.cryptofs; +import java.text.Normalizer; +import java.util.Collections; +import java.util.stream.Stream; + +import javax.inject.Inject; + +import com.google.common.base.Splitter; + import static java.util.Arrays.stream; import static java.util.Spliterator.IMMUTABLE; import static java.util.Spliterator.NONNULL; @@ -9,13 +17,6 @@ import static java.util.stream.StreamSupport.stream; import static org.cryptomator.cryptofs.Constants.SEPARATOR; -import java.util.Collections; -import java.util.Iterator; -import java.util.StringTokenizer; -import java.util.stream.Stream; - -import javax.inject.Inject; - @PerProvider class CryptoPathFactory { @@ -25,7 +26,7 @@ public CryptoPathFactory() { public CryptoPath getPath(CryptoFileSystemImpl fileSystem, String first, String... more) { boolean isAbsolute = first.startsWith(SEPARATOR); - Stream elements = Stream.concat(splitPath(first), stream(more).flatMap(this::splitPath)); + Stream elements = Stream.concat(Stream.of(first), stream(more)).flatMap(this::splitPath).map(this::normalize); return new CryptoPath(fileSystem, elements.collect(toList()), isAbsolute); } @@ -38,19 +39,12 @@ public CryptoPath rootFor(CryptoFileSystemImpl fileSystem) { } private Stream splitPath(String path) { - StringTokenizer tokenizer = new StringTokenizer(path, SEPARATOR); - Iterator iterator = new Iterator() { - @Override - public boolean hasNext() { - return tokenizer.hasMoreTokens(); - } - - @Override - public String next() { - return tokenizer.nextToken(); - } - }; - return stream(spliteratorUnknownSize(iterator, ORDERED | IMMUTABLE | NONNULL), false); + Iterable tokens = Splitter.on(SEPARATOR).omitEmptyStrings().split(path); + return stream(spliteratorUnknownSize(tokens.iterator(), ORDERED | IMMUTABLE | NONNULL), false); + } + + private String normalize(String str) { + return Normalizer.normalize(str, Normalizer.Form.NFC); } } diff --git a/src/test/java/org/cryptomator/cryptofs/DeletingFileVisitor.java b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java similarity index 62% rename from src/test/java/org/cryptomator/cryptofs/DeletingFileVisitor.java rename to src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java index 13b780fc..495ce3f4 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeletingFileVisitor.java +++ b/src/main/java/org/cryptomator/cryptofs/DeletingFileVisitor.java @@ -1,77 +1,76 @@ -/******************************************************************************* - * 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.attribute.PosixFilePermission.GROUP_READ; -import static java.nio.file.attribute.PosixFilePermission.GROUP_WRITE; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_READ; -import static java.nio.file.attribute.PosixFilePermission.OTHERS_WRITE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; -import static java.util.Arrays.asList; - -import java.io.IOException; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.DosFileAttributeView; -import java.nio.file.attribute.PosixFileAttributeView; -import java.nio.file.attribute.PosixFilePermission; -import java.util.HashSet; -import java.util.Set; - -class DeletingFileVisitor extends SimpleFileVisitor { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - forceDelete(file); - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - forceDelete(dir); - return FileVisitResult.CONTINUE; - } - - private void forceDelete(Path path) throws IOException { - setWritableSilently(path); - Files.deleteIfExists(path); - } - - private void setWritableSilently(Path path) { - try { - setWritable(path); - } catch (IOException e) { - // ignore - } - } - - private void setWritable(Path path) throws IOException { - DosFileAttributeView dosAttributes = Files.getFileAttributeView(path, DosFileAttributeView.class); - PosixFileAttributeView posixAttributes = Files.getFileAttributeView(path, PosixFileAttributeView.class); - if (dosAttributes != null) { - dosAttributes.setReadOnly(false); - dosAttributes.setSystem(false); - } - if (posixAttributes != null) { - posixAttributes.setPermissions(readWritePermissions()); - } - } - - private Set readWritePermissions() { - return new HashSet<>(asList( // - OWNER_READ, OWNER_WRITE, // - GROUP_READ, GROUP_WRITE, // - OTHERS_READ, OTHERS_WRITE)); - } - -} +/******************************************************************************* + * 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 java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.DosFileAttributeView; +import java.nio.file.attribute.PosixFileAttributeView; +import java.nio.file.attribute.PosixFilePermission; +import java.util.EnumSet; +import java.util.Set; + +import static java.nio.file.attribute.PosixFilePermission.*; + +class DeletingFileVisitor extends SimpleFileVisitor { + + public static final DeletingFileVisitor INSTANCE = new DeletingFileVisitor(); + + private static final Set POSIX_PERMISSIONS_770 = EnumSet.of(OWNER_WRITE, OWNER_READ, OWNER_EXECUTE, GROUP_WRITE, GROUP_READ, GROUP_EXECUTE); + + private DeletingFileVisitor() { + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + forceDeleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + forceDeleteIfExists(dir); + return FileVisitResult.CONTINUE; + } + + /** + * Tries to remove any write protection of the given file and tries to delete it afterwards. + * @param path Path ot a single file or directory. Will not be deleted recursively. + * @throws IOException exception thrown by delete. Any exceptions during removal of write protection will be ignored. + */ + static void forceDeleteIfExists(Path path) throws IOException { + setWritableSilently(path); + Files.deleteIfExists(path); + } + + private static void setWritableSilently(Path path) { + try { + setWritable(path); + } catch (IOException e) { + // ignore + } + } + + private static void setWritable(Path path) throws IOException { + DosFileAttributeView dosAttributes = Files.getFileAttributeView(path, DosFileAttributeView.class); + PosixFileAttributeView posixAttributes = Files.getFileAttributeView(path, PosixFileAttributeView.class); + if (dosAttributes != null) { + dosAttributes.setReadOnly(false); + dosAttributes.setSystem(false); + } + if (posixAttributes != null) { + posixAttributes.setPermissions(POSIX_PERMISSIONS_770); + } + } + +} diff --git a/src/main/java/org/cryptomator/cryptofs/ExceptionsDuringWrite.java b/src/main/java/org/cryptomator/cryptofs/ExceptionsDuringWrite.java index c82ca1a8..e0c72331 100644 --- a/src/main/java/org/cryptomator/cryptofs/ExceptionsDuringWrite.java +++ b/src/main/java/org/cryptomator/cryptofs/ExceptionsDuringWrite.java @@ -15,8 +15,16 @@ class ExceptionsDuringWrite { public ExceptionsDuringWrite() { } - public void add(IOException e) { + public synchronized void add(IOException e) { exceptions.add(e); } + public synchronized void throwIfPresent() throws IOException { + if (!exceptions.isEmpty()) { + IOException e = new IOException("Exceptions occured while writing"); + exceptions.forEach(e::addSuppressed); + throw e; + } + } + } diff --git a/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java b/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java index acffb64b..bbac9042 100644 --- a/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java +++ b/src/main/java/org/cryptomator/cryptofs/FinallyUtil.java @@ -16,16 +16,16 @@ public FinallyUtil() { @SuppressWarnings({"unchecked"}) public void guaranteeInvocationOf(RunnableThrowingException... tasks) throws E { - guaranteeInvocationOf(Arrays.stream(tasks)); + this.guaranteeInvocationOf(Arrays.stream(tasks)); } public void guaranteeInvocationOf(Iterable> tasks) throws E { - guaranteeInvocationOf(StreamSupport.stream(tasks.spliterator(), false)); + this.guaranteeInvocationOf(StreamSupport.stream(tasks.spliterator(), false)); } @SuppressWarnings({"unchecked"}) public void guaranteeInvocationOf(Stream> tasks) throws E { - guaranteeInvocationOf(tasks.map(t -> (RunnableThrowingException) t).iterator()); + this.guaranteeInvocationOf(tasks.map(t -> (RunnableThrowingException) t).iterator()); } @SuppressWarnings("unchecked") diff --git a/src/main/java/org/cryptomator/cryptofs/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/OpenCryptoFile.java index 29c3ee16..f42b867b 100644 --- a/src/main/java/org/cryptomator/cryptofs/OpenCryptoFile.java +++ b/src/main/java/org/cryptomator/cryptofs/OpenCryptoFile.java @@ -39,10 +39,12 @@ class OpenCryptoFile { private final OpenCounter openCounter; private final CryptoFileChannelFactory cryptoFileChannelFactory; private final CryptoFileSystemStats stats; + private final ExceptionsDuringWrite exceptionsDuringWrite; + private final FinallyUtil finallyUtil; @Inject public OpenCryptoFile(EffectiveOpenOptions options, Cryptor cryptor, FileChannel channel, FileHeader header, @OpenFileSize AtomicLong size, OpenCounter openCounter, CryptoFileChannelFactory cryptoFileChannelFactory, - ChunkCache chunkCache, @OpenFileOnCloseHandler Runnable onClose, CryptoFileSystemStats stats) { + ChunkCache chunkCache, @OpenFileOnCloseHandler Runnable onClose, CryptoFileSystemStats stats, ExceptionsDuringWrite exceptionsDuringWrite, FinallyUtil finallyUtil) { this.cryptor = cryptor; this.chunkCache = chunkCache; this.openCounter = openCounter; @@ -52,6 +54,8 @@ public OpenCryptoFile(EffectiveOpenOptions options, Cryptor cryptor, FileChannel this.header = header; this.size = size; this.stats = stats; + this.exceptionsDuringWrite = exceptionsDuringWrite; + this.finallyUtil = finallyUtil; } public FileChannel newFileChannel(EffectiveOpenOptions options) throws IOException { @@ -121,6 +125,11 @@ private void write(ByteSource source, long position) throws IOException { chunkData.copyDataStartingAt(offsetInChunk).from(source); chunkCache.set(chunkIndex, chunkData); } else { + /* + * TODO performance: + * We don't actually need to read the current data into the cache. + * It would suffice if store the written data and do reading when storing the chunk. + */ ChunkData chunkData = chunkCache.get(chunkIndex); chunkData.copyDataStartingAt(offsetInChunk).from(source); } @@ -148,9 +157,10 @@ public synchronized void truncate(long size) throws IOException { } public synchronized void force(boolean metaData, EffectiveOpenOptions options) throws IOException { - chunkCache.invalidateAll(); // TODO increase performance by writing chunks but keeping them cached + chunkCache.invalidateAll(); // TODO performance: write chunks but keep them cached if (options.writable()) { channel.write(cryptor.fileHeaderCryptor().encryptHeader(header), 0); + exceptionsDuringWrite.throwIfPresent(); } channel.force(metaData); } @@ -177,16 +187,15 @@ public void close() throws IOException { } public void close(EffectiveOpenOptions options) throws IOException { - force(true, options); - if (openCounter.countClose()) { - try { - onClose.run(); - } finally { - try { - channel.close(); - } finally { - cryptor.destroy(); - } + try { + force(true, options); + } finally { + if (openCounter.countClose()) { + finallyUtil.guaranteeInvocationOf( + () -> onClose.run(), + () -> channel.close(), + () -> cryptor.destroy() + ); } } } diff --git a/src/main/java/org/cryptomator/cryptofs/OpenCryptoFileFactoryModule.java b/src/main/java/org/cryptomator/cryptofs/OpenCryptoFileFactoryModule.java index 4cf13fc1..83fb5f43 100644 --- a/src/main/java/org/cryptomator/cryptofs/OpenCryptoFileFactoryModule.java +++ b/src/main/java/org/cryptomator/cryptofs/OpenCryptoFileFactoryModule.java @@ -41,10 +41,14 @@ public AtomicLong provideFileSize(FileChannel channel, Cryptor cryptor) { @Provides @PerOpenFile - public FileHeader provideFileHader(FileChannel channel, Cryptor cryptor, EffectiveOpenOptions options) { + public FileHeader provideFileHeader(FileChannel channel, Cryptor cryptor, EffectiveOpenOptions options) { return rethrowUnchecked(IOException.class).from(() -> { if (options.truncateExisting() || isNewFile(channel, options)) { - return cryptor.fileHeaderCryptor().create(); + FileHeader newHeader = cryptor.fileHeaderCryptor().create(); + channel.position(0); + channel.write(cryptor.fileHeaderCryptor().encryptHeader(newHeader)); + channel.force(false); + return newHeader; } else { ByteBuffer existingHeaderBuf = ByteBuffer.allocate(cryptor.fileHeaderCryptor().headerSize()); channel.position(0); diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java index 36a47b7b..d5a99ad8 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java @@ -5,25 +5,35 @@ *******************************************************************************/ package org.cryptomator.cryptofs.migration; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; -import org.cryptomator.cryptofs.migration.api.Migrator; -import org.cryptomator.cryptofs.migration.v6.Version6Migrator; -import org.cryptomator.cryptolib.CryptoLibModule; - import dagger.MapKey; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import org.cryptomator.cryptofs.migration.api.Migrator; +import org.cryptomator.cryptofs.migration.v6.Version6Migrator; +import org.cryptomator.cryptolib.api.CryptorProvider; -@Module(includes = {CryptoLibModule.class}) +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Module class MigrationModule { + private final CryptorProvider version1Cryptor; + + MigrationModule(CryptorProvider version1Cryptor) { + this.version1Cryptor = version1Cryptor; + } + + @Provides + CryptorProvider provideVersion1CryptorProvider() { + return version1Cryptor; + } + @Provides @IntoMap @MigratorKey(Migration.FIVE_TO_SIX) diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java index 78f2939a..f42d17dc 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java @@ -18,10 +18,10 @@ import org.cryptomator.cryptofs.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; +import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.KeyFile; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.cryptolib.common.SecureRandomModule; /** * Used to perform migration from an older vault format to a newer one. @@ -41,7 +41,7 @@ public class Migrators { private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder() // - .secureRandomModule(new SecureRandomModule(strongSecureRandom())) // + .migrationModule(new MigrationModule(Cryptors.version1(strongSecureRandom()))) // .build(); private final Map migrators; diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java index 5f6dc4d2..b4d5adfd 100644 --- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java +++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java @@ -17,8 +17,6 @@ import org.cryptomator.cryptofs.Constants; import org.cryptomator.cryptofs.migration.api.Migrator; -import org.cryptomator.cryptolib.api.CryptoLibVersion; -import org.cryptomator.cryptolib.api.CryptoLibVersion.Version; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.InvalidPassphraseException; @@ -34,7 +32,7 @@ public class Version6Migrator implements Migrator { private final CryptorProvider cryptorProvider; @Inject - public Version6Migrator(@CryptoLibVersion(Version.ONE) CryptorProvider cryptorProvider) { + public Version6Migrator(CryptorProvider cryptorProvider) { this.cryptorProvider = cryptorProvider; } diff --git a/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java index 26a85464..9239264d 100644 --- a/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java +++ b/src/test/java/org/cryptomator/cryptofs/AsyncDelegatingFileChannelTest.java @@ -45,9 +45,20 @@ public class AsyncDelegatingFileChannelTest { @Before public void setup() throws ReflectiveOperationException { channel = Mockito.mock(FileChannel.class); - Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); - channelOpenField.setAccessible(true); - channelOpenField.set(channel, true); + try { + Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); + channelOpenField.setAccessible(true); + channelOpenField.set(channel, true); + } catch (NoSuchFieldException e) { + // field only declared in jdk8 + } + try { + Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed"); + channelClosedField.setAccessible(true); + channelClosedField.set(channel, false); + } catch (NoSuchFieldException e) { + // field only declared in jdk 9 + } Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); channelCloseLockField.setAccessible(true); channelCloseLockField.set(channel, new Object()); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java index d1ffa13d..3d058e49 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoDirectoryStreamTest.java @@ -8,11 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import static org.mockito.AdditionalAnswers.returnsFirstArg; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - import java.io.IOException; import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream.Filter; @@ -29,30 +24,25 @@ import java.util.function.Consumer; import org.cryptomator.cryptofs.CryptoPathMapper.Directory; -import org.cryptomator.cryptolib.DaggerCryptoLibComponent; +import org.cryptomator.cryptofs.mocks.NullSecureRandom; +import org.cryptomator.cryptolib.Cryptors; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.FileNameCryptor; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.mockito.Mockito; -public class CryptoDirectoryStreamTest { +import static org.mockito.AdditionalAnswers.returnsFirstArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; - private static final Consumer DO_NOTHING_ON_CLOSE = ignored -> { - }; +public class CryptoDirectoryStreamTest { + + private static final Consumer DO_NOTHING_ON_CLOSE = ignored -> {}; private static final Filter ACCEPT_ALL = ignored -> true; - - private static CryptorProvider cryptorProvider; - - @BeforeClass - public static void setupClass() { - cryptorProvider = DaggerCryptoLibComponent.builder() // - .secureRandomModule(new TestSecureRandomModule()) // - .build() // - .version1(); - } + private static CryptorProvider CRYPTOR_PROVIDER = Cryptors.version1(NullSecureRandom.INSTANCE); private FileNameCryptor filenameCryptor; private Path ciphertextDirPath; @@ -66,7 +56,7 @@ public static void setupClass() { @Before @SuppressWarnings("unchecked") public void setup() throws IOException { - filenameCryptor = cryptorProvider.createNew().fileNameCryptor(); + filenameCryptor = CRYPTOR_PROVIDER.createNew().fileNameCryptor(); ciphertextDirPath = Mockito.mock(Path.class); FileSystem fs = Mockito.mock(FileSystem.class); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java index 53d4208d..7e4d16cb 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java @@ -78,6 +78,17 @@ public static void teardownClass() throws IOException { inMemoryFs.close(); } + // tests https://github.com/cryptomator/cryptofs/issues/22 + @Test + public void testFileSizeIsZeroAfterCreatingFileChannel() throws IOException { + long fileId = nextFileId(); + + try (FileChannel channel = writableChannel(fileId)) { + assertEquals(0, channel.size()); + assertEquals(0, Files.size(filePath(fileId))); + } + } + @Test public void testWriteAndReadNothing() throws IOException { long fileId = nextFileId(); @@ -254,16 +265,20 @@ private long nextFileId() { return nextFileId++; } + private Path filePath(long fileId) { + return fileSystem.getPath("/test" + fileId + ".file"); + } + private FileChannel readableChannel(long fileId) throws IOException { - return FileChannel.open(fileSystem.getPath("/test" + fileId + ".file"), READ); + return FileChannel.open(filePath(fileId), READ); } private FileChannel writableChannel(long fileId) throws IOException { - return FileChannel.open(fileSystem.getPath("/test" + fileId + ".file"), CREATE, WRITE); + return FileChannel.open(filePath(fileId), CREATE, WRITE); } private FileChannel writableChannelInAppendMode(long fileId) throws IOException { - return FileChannel.open(fileSystem.getPath("/test" + fileId + ".file"), CREATE, WRITE, APPEND); + return FileChannel.open(filePath(fileId), CREATE, WRITE, APPEND); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java index a220a65d..0c8f5f81 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java @@ -85,7 +85,7 @@ public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException assertThat(parsed.pathToVault(), is(absolutePathToVault)); assertThat(parsed.pathInsideVault(), is("/a/b")); } finally { - Files.walkFileTree(tempDir, new DeletingFileVisitor()); + Files.walkFileTree(tempDir, DeletingFileVisitor.INSTANCE); } } diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathFactoryTest.java new file mode 100644 index 00000000..8a6e5688 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathFactoryTest.java @@ -0,0 +1,79 @@ +package org.cryptomator.cryptofs; + +import java.text.Normalizer; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class CryptoPathFactoryTest { + + private final CryptoFileSystemImpl cryptoFileSystem = Mockito.mock(CryptoFileSystemImpl.class); + private final CryptoPathFactory factory = new CryptoPathFactory(); + + @Test + public void testEmptyFor() { + CryptoPath path = factory.emptyFor(cryptoFileSystem); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[0], path.getElements().toArray()); + Assert.assertFalse(path.isAbsolute()); + } + + @Test + public void testRootFor() { + CryptoPath path = factory.rootFor(cryptoFileSystem); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[0], path.getElements().toArray()); + Assert.assertTrue(path.isAbsolute()); + } + + @Test + public void testGetRelativePathWithSingleArgument() { + CryptoPath path = factory.getPath(cryptoFileSystem, "foo/bar//baz"); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[]{"foo", "bar", "baz"}, path.getElements().toArray()); + Assert.assertFalse(path.isAbsolute()); + } + + @Test + public void testGetAbsolutePathWithSingleArgument() { + CryptoPath path = factory.getPath(cryptoFileSystem, "/foo/bar//baz"); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[]{"foo", "bar", "baz"}, path.getElements().toArray()); + Assert.assertTrue(path.isAbsolute()); + } + + @Test + public void testGetRelativePathWithMultiArgument() { + CryptoPath path = factory.getPath(cryptoFileSystem, "foo/bar", "//baz/foo", "bar"); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[]{"foo", "bar", "baz", "foo", "bar"}, path.getElements().toArray()); + Assert.assertFalse(path.isAbsolute()); + } + + @Test + public void testGetAbsolutePathWithMultiArgument() { + CryptoPath path = factory.getPath(cryptoFileSystem, "/foo/bar", "//baz/foo", "bar"); + + Assert.assertEquals(cryptoFileSystem, path.getFileSystem()); + Assert.assertArrayEquals(new String[]{"foo", "bar", "baz", "foo", "bar"}, path.getElements().toArray()); + Assert.assertTrue(path.isAbsolute()); + } + + @Test + public void testUnicodeNormalization() { + String nfcChar = Normalizer.normalize("ü", Normalizer.Form.NFC); + String nfdChar = Normalizer.normalize("ü", Normalizer.Form.NFD); + Assert.assertNotEquals(nfcChar, nfdChar); + + CryptoPath path = factory.getPath(cryptoFileSystem, nfdChar, nfdChar, nfcChar); + + Assert.assertArrayEquals(new String[]{nfcChar, nfcChar, nfcChar}, path.getElements().toArray()); + } + +} diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java index 42fb5d6f..1078b14e 100644 --- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java @@ -55,7 +55,7 @@ public static void setupClass() throws IOException { @AfterClass public static void teardownClass() throws IOException { - walkFileTree(tempDir, new DeletingFileVisitor()); + walkFileTree(tempDir, DeletingFileVisitor.INSTANCE); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java index 6e43e0f5..f656fe21 100644 --- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java @@ -1,16 +1,5 @@ package org.cryptomator.cryptofs; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.io.IOException; import java.lang.reflect.Field; import java.nio.ByteBuffer; @@ -27,6 +16,17 @@ import org.junit.rules.ExpectedException; import org.mockito.Mockito; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class DirectoryIdLoaderTest { @Rule @@ -111,9 +111,20 @@ public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException, Refl private FileChannel createFileChannelMock() throws ReflectiveOperationException { FileChannel channel = Mockito.mock(FileChannel.class); - Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); - channelOpenField.setAccessible(true); - channelOpenField.set(channel, true); + try { + Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open"); + channelOpenField.setAccessible(true); + channelOpenField.set(channel, true); + } catch (NoSuchFieldException e) { + // field only declared in jdk8 + } + try { + Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed"); + channelClosedField.setAccessible(true); + channelClosedField.set(channel, false); + } catch (NoSuchFieldException e) { + // field only declared in jdk 9 + } Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock"); channelCloseLockField.setAccessible(true); channelCloseLockField.set(channel, new Object()); diff --git a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java index d5936b01..9acdf781 100644 --- a/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/LongFileNameProviderTest.java @@ -33,7 +33,7 @@ public void setup() throws IOException { @After public void teardown() throws IOException { - Files.walkFileTree(tmpPath, new DeletingFileVisitor()); + Files.walkFileTree(tmpPath, DeletingFileVisitor.INSTANCE); } private int countFiles(Path dir) throws IOException { diff --git a/src/test/java/org/cryptomator/cryptofs/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/OpenCryptoFileTest.java index 24bb685c..ba0e12ec 100644 --- a/src/test/java/org/cryptomator/cryptofs/OpenCryptoFileTest.java +++ b/src/test/java/org/cryptomator/cryptofs/OpenCryptoFileTest.java @@ -2,9 +2,7 @@ import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.io.IOException; import java.nio.ByteBuffer; @@ -19,6 +17,7 @@ import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileContentCryptor; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.FileHeaderCryptor; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.theories.Theories; @@ -49,8 +48,10 @@ public class OpenCryptoFileTest { private OpenCounter openCounter = mock(OpenCounter.class); private CryptoFileChannelFactory cryptoFileChannelFactory = mock(CryptoFileChannelFactory.class); private CryptoFileSystemStats stats = mock(CryptoFileSystemStats.class); + private ExceptionsDuringWrite exceptionsDuringWrite = mock(ExceptionsDuringWrite.class); + private FinallyUtil finallyUtil = new FinallyUtil(); - private OpenCryptoFile inTest = new OpenCryptoFile(options, cryptor, channel, header, size, openCounter, cryptoFileChannelFactory, chunkCache, onClose, stats); + private OpenCryptoFile inTest = new OpenCryptoFile(options, cryptor, channel, header, size, openCounter, cryptoFileChannelFactory, chunkCache, onClose, stats, exceptionsDuringWrite, finallyUtil); @Theory public void testLockDelegatesToChannel(boolean shared) throws IOException { @@ -114,6 +115,50 @@ public void testCloseDelegatesToCryptoFileChannelFactory() throws IOException { verify(cryptoFileChannelFactory).close(); } + @Test + public void testForceThrowsExceptionDuringWriteIfWritable() throws IOException { + when(options.writable()).thenReturn(true); + IOException expected = new IOException(); + doThrow(expected).when(exceptionsDuringWrite).throwIfPresent(); + FileHeaderCryptor fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); + when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); + + thrown.expect(is(expected)); + + inTest.force(false, options); + } + + @Test + public void testForceDoesNotThrowExceptionDuringWriteIfNotWritable() throws IOException { + when(options.writable()).thenReturn(false); + IOException expected = new IOException(); + doThrow(expected).when(exceptionsDuringWrite).throwIfPresent(); + + inTest.force(false, options); + } + + @Test + public void testCloseThrowsExceptionDuringWriteIfWritable() throws IOException { + when(options.writable()).thenReturn(true); + IOException expected = new IOException(); + doThrow(expected).when(exceptionsDuringWrite).throwIfPresent(); + FileHeaderCryptor fileHeaderCryptor = Mockito.mock(FileHeaderCryptor.class); + when(cryptor.fileHeaderCryptor()).thenReturn(fileHeaderCryptor); + + thrown.expect(is(expected)); + + inTest.close(options); + } + + @Test + public void testCloseDoesNotThrowExceptionDuringWriteIfNotWritable() throws IOException { + when(options.writable()).thenReturn(false); + IOException expected = new IOException(); + doThrow(expected).when(exceptionsDuringWrite).throwIfPresent(); + + inTest.close(options); + } + @Test public void testRead() throws IOException { int cleartextChunkSize = 1000; // 1 kb per chunk diff --git a/src/test/java/org/cryptomator/cryptofs/PathMatcherFactoryTest.java b/src/test/java/org/cryptomator/cryptofs/PathMatcherFactoryTest.java index 6d210c09..62cdf018 100644 --- a/src/test/java/org/cryptomator/cryptofs/PathMatcherFactoryTest.java +++ b/src/test/java/org/cryptomator/cryptofs/PathMatcherFactoryTest.java @@ -1,12 +1,5 @@ package org.cryptomator.cryptofs; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.nio.file.PathMatcher; import org.junit.Rule; @@ -15,6 +8,13 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public class PathMatcherFactoryTest { @Rule diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java index 1663a5a1..fb4f6a01 100644 --- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java @@ -35,7 +35,7 @@ public void setup() throws IOException { @After public void teardown() throws IOException { - Files.walkFileTree(storageLocation, new DeletingFileVisitor()); + Files.walkFileTree(storageLocation, DeletingFileVisitor.INSTANCE); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java index 4d316e90..6f7baf46 100644 --- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java +++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java @@ -46,7 +46,7 @@ public static void setupClass() throws IOException { @AfterClass public static void teardownClass() throws IOException { - walkFileTree(tempDir, new DeletingFileVisitor()); + walkFileTree(tempDir, DeletingFileVisitor.INSTANCE); } @Test diff --git a/src/test/java/org/cryptomator/cryptofs/TestSecureRandomModule.java b/src/test/java/org/cryptomator/cryptofs/TestSecureRandomModule.java deleted file mode 100644 index 5447b1a1..00000000 --- a/src/test/java/org/cryptomator/cryptofs/TestSecureRandomModule.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.cryptomator.cryptofs; - -import java.security.SecureRandom; -import java.util.Arrays; - -import org.cryptomator.cryptolib.common.SecureRandomModule; - -class TestSecureRandomModule extends SecureRandomModule { - - private static final SecureRandom NULL_RANDOM = new SecureRandom() { - @Override - public synchronized void nextBytes(byte[] bytes) { - Arrays.fill(bytes, (byte) 0x00); - }; - }; - - public TestSecureRandomModule() { - super(null); - } - - @Override - public SecureRandom provideFastSecureRandom(SecureRandom ignored) { - return NULL_RANDOM; - } - - @Override - public SecureRandom provideNativeSecureRandom() { - return NULL_RANDOM; - } - -} diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java index ff150644..3ab4a116 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java @@ -3,7 +3,7 @@ import java.security.NoSuchAlgorithmException; import org.cryptomator.cryptofs.mocks.NullSecureRandom; -import org.cryptomator.cryptolib.common.SecureRandomModule; +import org.cryptomator.cryptolib.Cryptors; import org.junit.Assert; import org.junit.Test; @@ -11,8 +11,8 @@ public class MigrationComponentTest { @Test public void testAvailableMigrators() throws NoSuchAlgorithmException { - SecureRandomModule secRndModule = new SecureRandomModule(new NullSecureRandom()); - TestMigrationComponent comp = DaggerTestMigrationComponent.builder().secureRandomModule(secRndModule).build(); + MigrationModule migrationModule = new MigrationModule(Cryptors.version1(NullSecureRandom.INSTANCE)); + TestMigrationComponent comp = DaggerTestMigrationComponent.builder().migrationModule(migrationModule).build(); Assert.assertFalse(comp.availableMigrators().isEmpty()); } diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java index bb4b5246..08cd98e4 100644 --- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java +++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java @@ -31,7 +31,7 @@ public class Version6MigratorTest { @Before public void setup() throws IOException { - cryptorProvider = Cryptors.version1(new NullSecureRandom()); + cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE); fs = Jimfs.newFileSystem(Configuration.unix()); pathToVault = fs.getPath("/vaultDir"); masterkeyFile = pathToVault.resolve("masterkey.cryptomator");