Skip to content

Commit

Permalink
Merge branch 'release/1.9.12'
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Jun 18, 2020
2 parents 772ea4f + 76f43a5 commit 4a0ec07
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 62 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>cryptofs</artifactId>
<version>1.9.11</version>
<version>1.9.12</version>
<name>Cryptomator Crypto Filesystem</name>
<description>This library provides the Java filesystem provider used by Cryptomator.</description>
<url>https://github.com/cryptomator/cryptofs</url>
Expand Down
16 changes: 4 additions & 12 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import org.cryptomator.cryptofs.attr.AttributeComponent;
import org.cryptomator.cryptofs.attr.AttributeViewComponent;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
import org.cryptomator.cryptolib.api.Cryptor;
Expand All @@ -26,8 +26,6 @@
import java.nio.file.Path;
import java.util.Optional;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

@Module(subcomponents = {AttributeComponent.class, AttributeViewComponent.class, OpenCryptoFileComponent.class, DirectoryStreamComponent.class})
class CryptoFileSystemModule {

Expand All @@ -40,9 +38,10 @@ public Cryptor provideCryptor(CryptorProvider cryptorProvider, @PathToVault Path
Path masterKeyPath = pathToVault.resolve(properties.masterkeyFilename());
assert Files.exists(masterKeyPath); // since 1.3.0 a file system can only be created for existing vaults. initialization is done before.
byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
Path backupKeyPath = pathToVault.resolve(properties.masterkeyFilename() + MasterkeyBackupFileHasher.generateFileIdSuffix(keyFileContents) + Constants.MASTERKEY_BACKUP_SUFFIX);
Cryptor cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(keyFileContents), properties.passphrase(), properties.pepper(), Constants.VAULT_VERSION);
backupMasterkeyFileIfRequired(masterKeyPath, backupKeyPath, readonlyFlag);
if (!readonlyFlag.isSet()) {
MasterkeyBackupHelper.backupMasterKey(masterKeyPath);
}
return cryptor;
} catch (IOException e) {
throw new UncheckedIOException(e);
Expand All @@ -59,11 +58,4 @@ public Optional<FileStore> provideNativeFileStore(@PathToVault Path pathToVault)
return Optional.empty();
}
}

private void backupMasterkeyFileIfRequired(Path masterKeyPath, Path backupKeyPath, ReadonlyFlag readonlyFlag) throws IOException {
if (!readonlyFlag.isSet()) {
Files.copy(masterKeyPath, backupKeyPath, REPLACE_EXISTING);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
import org.cryptomator.cryptolib.Cryptors;
import org.cryptomator.cryptolib.api.Cryptor;
Expand Down Expand Up @@ -234,7 +233,7 @@ public static void changePassphrase(Path pathToVault, String masterkeyFilename,
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath);
byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, pepper, normalizedOldPassphrase, normalizedNewPassphrase);
Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE);
Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE);
}
Expand Down Expand Up @@ -271,7 +270,7 @@ public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byt
Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
if (Files.exists(masterKeyPath)) {
byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath);
Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE);
}
Files.write(masterKeyPath, masterKeyBytes, CREATE_NEW, WRITE);
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemCompo
public synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException {
try {
Path normalizedPathToVault = pathToVault.normalize();
CryptoFileSystemProperties adjustedProperites = adjustForCapabilities(normalizedPathToVault, properties);
CryptoFileSystemProperties adjustedProperties = adjustForCapabilities(normalizedPathToVault, properties);
return fileSystems.compute(normalizedPathToVault, (key, value) -> {
if (value == null) {
return cryptoFileSystemComponentBuilder //
.pathToVault(key) //
.properties(adjustedProperites) //
.properties(adjustedProperties) //
.provider(provider) //
.build() //
.cryptoFileSystem();
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.cryptomator.cryptofs.common;

import com.google.common.io.BaseEncoding;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
* Utility class for generating a suffix for the backup file to make it unique to its original master key file.
*/
public final class MasterkeyBackupHelper {

private static final Logger LOG = LoggerFactory.getLogger(MasterkeyBackupHelper.class);

/**
* Computes the SHA-256 digest of the given byte array and returns a file suffix containing the first 4 bytes in hex string format.
*
* @param fileBytes the input byte for which the digest is computed
* @return "." + first 4 bytes of SHA-256 digest in hex string format
*/
public static String generateFileIdSuffix(byte[] fileBytes) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(fileBytes);
return "." + BaseEncoding.base16().encode(digest, 0, 4);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Every Java Platform must support the Message Digest algorithm SHA-256", e);
}
}

/**
* Do a best-effort attempt to backup the masterkey at the given path. Fail silently if a valid backup already exists.
*
* @param masterKeyPath The masterkey file to backup
* @throws IOException Any non-recoverable I/O exception that occurs during this attempt
*/
public static Path backupMasterKey(Path masterKeyPath) throws IOException {
byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
String backupFileName = masterKeyPath.getFileName().toString() + generateFileIdSuffix(keyFileContents) + Constants.MASTERKEY_BACKUP_SUFFIX;
Path backupFilePath = masterKeyPath.resolveSibling(backupFileName);
try (WritableByteChannel ch = Files.newByteChannel(backupFilePath, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
ch.write(ByteBuffer.wrap(keyFileContents));
} catch (AccessDeniedException e) {
LOG.info("Storage device does not allow writing backup file. Comparing masterkey with backup directly.");
assertExistingBackupMatchesContent(backupFilePath, ByteBuffer.wrap(keyFileContents));
}
return backupFilePath;
}

private static void assertExistingBackupMatchesContent(Path backupFilePath, ByteBuffer expectedContent) throws IOException {
if (Files.exists(backupFilePath)) {
// TODO replace by Files.mismatch() when using JDK > 12
ByteBuffer buf = ByteBuffer.allocateDirect(expectedContent.remaining() + 1);
try (ReadableByteChannel ch = Files.newByteChannel(backupFilePath, StandardOpenOption.READ)) {
ch.read(buf);
buf.flip();
if (buf.compareTo(expectedContent) != 0) {
throw new IllegalStateException("Corrupt masterkey backup: " + backupFilePath);
}
LOG.debug("Verified backup file: {}", backupFilePath);
} catch (NoSuchFileException e) {
LOG.warn("Did not find backup file: {}", backupFilePath);
}
}
}
}
16 changes: 9 additions & 7 deletions src/main/java/org/cryptomator/cryptofs/fh/ChunkData.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ public CopyWithoutDirection copyDataStartingAt(int offset) {
return new CopyWithoutDirection() {
@Override
public void to(ByteBuffer target) {
bytes.limit(min(length, target.remaining() + offset));
bytes.position(offset);
target.put(bytes);
ByteBuffer buf = bytes.asReadOnlyBuffer();
buf.limit(min(length, target.remaining() + offset));
buf.position(offset);
target.put(buf);
}

@Override
Expand All @@ -65,11 +66,12 @@ public void from(ByteBuffer source) {

@Override
public void from(ByteSource source) {
ByteBuffer buf = bytes.duplicate();
buf.limit(buf.capacity());
buf.position(offset);
source.copyTo(buf);
dirty = true;
bytes.limit(bytes.capacity());
bytes.position(offset);
source.copyTo(bytes);
length = max(length, bytes.position());
length = max(length, buf.position());
}
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import javax.inject.Inject;

import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
Expand Down Expand Up @@ -48,8 +48,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp
KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 5)) {
// create backup, as soon as we know the password was correct:
Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
Path masterkeyBackupFile = MasterkeyBackupHelper.backupMasterKey(masterkeyFile);
LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());

progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.common.DeletingFileVisitor;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptofs.common.MasterkeyBackupFileHasher;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationEvent;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
Expand Down Expand Up @@ -52,8 +52,7 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp
KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) {
// create backup, as soon as we know the password was correct:
Path masterkeyBackupFile = vaultRoot.resolve(masterkeyFilename + MasterkeyBackupFileHasher.generateFileIdSuffix(fileContentsBeforeUpgrade) + Constants.MASTERKEY_BACKUP_SUFFIX);
Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
Path masterkeyBackupFile = MasterkeyBackupHelper.backupMasterKey(masterkeyFile);
LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());

// check file system capabilities:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.cryptomator.cryptofs.common;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.Random;
import java.util.stream.Stream;

class MasterkeyBackupHelperTest {

@EnabledOnOs({OS.LINUX, OS.MAC})
@ParameterizedTest
@MethodSource("createRandomBytes")
public void testBackupFilePosix(byte[] contents, @TempDir Path tmp) throws IOException {
Path originalFile = tmp.resolve("original");
Files.write(originalFile, contents);

Path backupFile = MasterkeyBackupHelper.backupMasterKey(originalFile);
Assertions.assertArrayEquals(contents, Files.readAllBytes(backupFile));

Files.setPosixFilePermissions(backupFile, PosixFilePermissions.fromString("r--r--r--"));
Path backupFile2 = MasterkeyBackupHelper.backupMasterKey(originalFile);
Assertions.assertEquals(backupFile, backupFile2);
}

@EnabledOnOs({OS.WINDOWS})
@ParameterizedTest
@MethodSource("createRandomBytes")
public void testBackupFileWin(byte[] contents, @TempDir Path tmp) throws IOException {
Path originalFile = tmp.resolve("original");
Files.write(originalFile, contents);

Path backupFile = MasterkeyBackupHelper.backupMasterKey(originalFile);
Assertions.assertArrayEquals(contents, Files.readAllBytes(backupFile));

Files.getFileAttributeView(backupFile, DosFileAttributeView.class).setReadOnly(true);
Path backupFile2 = MasterkeyBackupHelper.backupMasterKey(originalFile);
Assertions.assertEquals(backupFile, backupFile2);
}

static Stream<byte[]> createRandomBytes() {
Random rnd = new Random(42l);
return Stream.generate(() -> {
byte[] bytes = new byte[100];
rnd.nextBytes(bytes);
return bytes;
}).limit(10);
}

}
39 changes: 39 additions & 0 deletions src/test/java/org/cryptomator/cryptofs/fh/ChunkDataTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
import org.junit.jupiter.params.provider.MethodSource;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.Stream;

import static org.cryptomator.cryptofs.matchers.ByteBufferMatcher.contains;
Expand Down Expand Up @@ -121,6 +127,39 @@ public void testCopyToWithOffsetCopiesContentFromOffset(WayToCreateChunkDataWith
MatcherAssert.assertThat(target, contains(repeat(0).times(offset).asByteBuffer()));
}

@Test // https://github.com/cryptomator/cryptofs/issues/85
public void testRaceConditionsDuringRead() throws InterruptedException {
ByteBuffer src = StandardCharsets.US_ASCII.encode("abcdefg");
ChunkData inTest = ChunkData.wrap(src);
int attempts = 4000;
int threads = 6;

CountDownLatch cdl = new CountDownLatch(attempts);
ExecutorService executor = Executors.newFixedThreadPool(threads);
LongAdder successfulTests = new LongAdder();

for (int i = 0; i < attempts; i++) {
int offset = i % 7;
char expected = "abcdefg".charAt(offset);
executor.execute(() -> {
try {
ByteBuffer dst = ByteBuffer.allocate(1);
inTest.copyDataStartingAt(offset).to(dst);
dst.flip();
char actual = StandardCharsets.US_ASCII.decode(dst).charAt(0);
if (expected == actual) {
successfulTests.increment();
}
} finally {
cdl.countDown();
}
});
}

Assertions.assertTimeoutPreemptively(Duration.ofSeconds(5), () -> cdl.await());
Assertions.assertEquals(attempts, successfulTests.sum());
}

interface WayToCreateEmptyChunkData {

ChunkData create();
Expand Down
Loading

0 comments on commit 4a0ec07

Please sign in to comment.