Merge branch 'develop' into feature/hub

# Conflicts:
#	.github/workflows/release.yml
#	pom.xml
#	src/main/resources/license/THIRD-PARTY.txt
This commit is contained in:
Sebastian Stenzel
2022-02-18 09:38:25 +01:00
74 changed files with 924 additions and 157 deletions

View File

@@ -49,6 +49,12 @@ public class KeychainManager implements KeychainAccessProvider {
setPassphraseStored(key, true);
}
@Override
public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
getKeychainOrFail().storePassphrase(key, displayName, passphrase);
setPassphraseStored(key, true);
}
@Override
public char[] loadPassphrase(String key) throws KeychainAccessException {
char[] passphrase = getKeychainOrFail().loadPassphrase(key);
@@ -70,6 +76,14 @@ public class KeychainManager implements KeychainAccessProvider {
}
}
@Override
public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
if (isPassphraseStored(key)) {
getKeychainOrFail().changePassphrase(key, displayName, passphrase);
setPassphraseStored(key, true);
}
}
@Override
public boolean isSupported() {
return keychain.getValue() != null;

View File

@@ -5,6 +5,9 @@ import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.vaults.Volume;
import javax.inject.Inject;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
@@ -27,4 +30,13 @@ class CustomDriveLetterChooser implements MountPointChooser {
public Optional<Path> chooseMountPoint(Volume caller) {
return this.vaultSettings.getWinDriveLetter().map(letter -> letter.charAt(0) + ":\\").map(Paths::get);
}
@Override
public boolean prepare(Volume caller, Path driveLetter) throws InvalidMountPointException {
if (!Files.notExists(driveLetter, LinkOption.NOFOLLOW_LINKS)) {
//Drive already exists OR can't be determined
throw new InvalidMountPointException(new FileAlreadyExistsException(driveLetter.toString()));
}
return false;
}
}

View File

@@ -56,7 +56,7 @@ class CustomMountPointChooser implements MountPointChooser {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}

View File

@@ -66,9 +66,9 @@ class MountPointHelper {
private void clearIrregularUnmountDebris(Path dirContainingMountPoints) {
IOException cleanupFailed = new IOException("Cleanup failed");
try {
try (var ds = Files.newDirectoryStream(dirContainingMountPoints)) {
LOG.debug("Performing cleanup of mountpoint dir {}.", dirContainingMountPoints);
for (Path p : Files.newDirectoryStream(dirContainingMountPoints)) {
for (Path p : ds) {
try {
var attr = Files.readAttributes(p, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
if (attr.isOther() && attr.isDirectory()) { // yes, this is possible with windows junction points -.-
@@ -113,8 +113,10 @@ class MountPointHelper {
}
private void ensureIsEmpty(Path dir) throws IOException {
if (Files.newDirectoryStream(dir).iterator().hasNext()) {
throw new DirectoryNotEmptyException(dir.toString());
try (var ds = Files.newDirectoryStream(dir)) {
if (ds.iterator().hasNext()){
throw new DirectoryNotEmptyException(dir.toString());
}
}
}
}

View File

@@ -65,7 +65,7 @@ class TemporaryMountPointChooser implements MountPointChooser {
throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement"));
}
default -> {
//Currently the case for "PARENT_OPT_MOUNT_POINT"
//Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT"
throw new InvalidMountPointException(new IllegalStateException("Not implemented"));
}
}

View File

@@ -86,7 +86,7 @@ public class DokanyVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return MountPointRequirement.EMPTY_MOUNT_POINT;
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.EMPTY_MOUNT_POINT;
}
public static boolean isSupportedStatic() {

View File

@@ -4,6 +4,7 @@ import com.google.common.collect.Iterators;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
@@ -28,11 +29,14 @@ public class FuseVolume extends AbstractVolume {
private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class);
private static final Pattern NON_WHITESPACE_OR_QUOTED = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'"); // Thanks to https://stackoverflow.com/a/366532
private final VaultSettings vaultSettings;
private Mount mount;
@Inject
public FuseVolume(@Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
public FuseVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers);
this.vaultSettings = vaultSettings;
}
@Override
@@ -50,7 +54,7 @@ public class FuseVolume extends AbstractVolume {
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build();
this.mount = mounter.mount(root, envVars, onExitAction);
} catch ( FuseMountException | FuseNotSupportedException e) {
} catch (FuseMountException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e);
}
}
@@ -119,7 +123,10 @@ public class FuseVolume extends AbstractVolume {
@Override
public MountPointRequirement getMountPointRequirement() {
return SystemUtils.IS_OS_WINDOWS ? MountPointRequirement.PARENT_NO_MOUNT_POINT : MountPointRequirement.EMPTY_MOUNT_POINT;
if (!SystemUtils.IS_OS_WINDOWS) {
return MountPointRequirement.EMPTY_MOUNT_POINT;
}
return this.vaultSettings.getWinDriveLetter().isPresent() ? MountPointRequirement.UNUSED_ROOT_DIR : MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public static boolean isSupportedStatic() {

View File

@@ -6,6 +6,11 @@ package org.cryptomator.common.vaults;
*/
public enum MountPointRequirement {
/**
* The Mountpoint needs to be a filesystem root and must not exist.
*/
UNUSED_ROOT_DIR,
/**
* No Mountpoint on the local filesystem required. (e.g. WebDAV)
*/

View File

@@ -7,9 +7,11 @@ import java.io.EOFException;
import java.io.IOException;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.AlreadyBoundException;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.UnsupportedAddressTypeException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Executor;
@@ -29,10 +31,18 @@ class Server implements IpcCommunicator {
public static Server create(Path socketPath) throws IOException {
Files.createDirectories(socketPath.getParent());
var address = UnixDomainSocketAddress.of(socketPath);
var serverSocketChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
serverSocketChannel.bind(address);
LOG.info("Spawning IPC server listening on socket {}", socketPath);
return new Server(serverSocketChannel, socketPath);
ServerSocketChannel ch = null;
try {
ch = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
ch.bind(address);
LOG.info("Spawning IPC server listening on socket {}", socketPath);
return new Server(ch, socketPath);
} catch (IOException | AlreadyBoundException | UnsupportedAddressTypeException e) {
if (ch != null) {
ch.close();
}
throw e;
}
}
@Override

View File

@@ -79,8 +79,9 @@ public class LoggerModule {
@Singleton
@Named("fileAppender")
static Appender<ILoggingEvent> provideFileAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
if (environment.getLogDir().isPresent()) {
Path logDir = environment.getLogDir().get();
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
Path logDir = optionalLogDir.get();
RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
appender.setContext(context);
appender.setFile(logDir.resolve(LOGFILE_NAME).toString());
@@ -110,9 +111,10 @@ public class LoggerModule {
@Singleton
@Named("upgradeAppender")
static Appender<ILoggingEvent> provideUpgradeAppender(LoggerContext context, PatternLayoutEncoder encoder, Environment environment) {
if (environment.getLogDir().isPresent()) {
var optionalLogDir = environment.getLogDir();
if (optionalLogDir.isPresent()) {
FileAppender<ILoggingEvent> appender = new FileAppender<>();
appender.setFile(environment.getLogDir().get().resolve(UPGRADE_FILENAME).toString());
appender.setFile(optionalLogDir.get().resolve(UPGRADE_FILENAME).toString());
appender.setContext(context);
appender.setEncoder(encoder);
appender.start();

View File

@@ -102,7 +102,7 @@ public class ChangePasswordController implements FxController {
private void updatePasswordInSystemkeychain() {
if (keychain.isSupported() && !keychain.isLocked()) {
try {
keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters());
keychain.changePassphrase(vault.getId(), vault.getDisplayName(), newPasswordController.passwordField.getCharacters());
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
} catch (KeychainAccessException e) {
LOG.error("Failed to update password in system keychain.", e);

View File

@@ -4,30 +4,35 @@ import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UserInteractionLock<E extends Enum> {
public class UserInteractionLock<E extends Enum<E>> {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
private volatile E state;
private final AtomicBoolean interacted = new AtomicBoolean();
private final AtomicReference<E> state;
public UserInteractionLock(E initialValue) {
this.state = initialValue;
this.state = new AtomicReference<>(initialValue);
}
public synchronized void reset(E value) {
this.state = value;
state.set(value);
interacted.set(false);
}
public void interacted(E result) {
assert Platform.isFxApplicationThread();
lock.lock();
try {
state = result;
state.set(result);
interacted.set(true);
awaitingInteraction.set(false);
condition.signal();
} finally {
@@ -40,8 +45,10 @@ public class UserInteractionLock<E extends Enum> {
lock.lock();
try {
Platform.runLater(() -> awaitingInteraction.set(true));
condition.await();
return state;
while (!interacted.get()) {
condition.await();
}
return state.get();
} finally {
lock.unlock();
}

View File

@@ -23,7 +23,7 @@ public class UpdateCheckerTask extends Task<String> {
private static final Logger LOG = LoggerFactory.getLogger(UpdateCheckerTask.class);
private static final long MAX_RESPONSE_SIZE = 10 * 1024; // 10kb should be sufficient. protect against flooding
private static final long MAX_RESPONSE_SIZE = 10L * 1024; // 10kb should be sufficient. protect against flooding
private static final Gson GSON = new GsonBuilder().setLenient().create();
private final HttpClient httpClient;

View File

@@ -47,7 +47,7 @@ class MasterkeyFileLoadingFinisher {
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}

View File

@@ -23,8 +23,10 @@ import org.slf4j.LoggerFactory;
import javax.inject.Named;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.nio.file.Path;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -93,15 +95,27 @@ public abstract class MasterkeyFileLoadingModule {
@Provides
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
@KeyLoadingScoped
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName()));
}
});
return scene;
}
@Provides
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
@KeyLoadingScoped
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders, @KeyLoading Stage window, @KeyLoading Vault v, ResourceBundle resourceBundle) {
var scene = fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
scene.windowProperty().addListener((prop, oldVal, newVal) -> {
if (window.equals(newVal)) {
window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName()));
}
});
return scene;
}
@Binds

View File

@@ -3,6 +3,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.common.BackupHelper;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
@@ -20,6 +21,7 @@ import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
@@ -61,14 +63,24 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
@Override
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
try {
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
if (!Files.exists(filePath)) {
filePath = getAlternateMasterkeyFilePath();
}
CharSequence passphrase = getPassphrase();
return masterkeyFileAccess.load(filePath, passphrase);
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
//backup
if (filePath.startsWith(vault.getPath())) {
try {
BackupHelper.attemptBackup(filePath);
} catch (IOException e) {
LOG.warn("Unable to create backup for masterkey file.");
}
} else {
LOG.info("Masterkey file not stored inside vault. Not creating a backup.");
}
return masterkey;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Unlock interrupted", e);

View File

@@ -127,10 +127,10 @@ public class VaultStatisticsController implements FxController {
encryptedBytesWrite.getData().add(new Data<>(currentStep, encBytes));
// adjust ranges:
readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS);
readChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
readChartXAxis.setUpperBound(currentStep);
readChartYAxis.setUpperBound(allTimeMax);
writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS);
writeChartXAxis.setLowerBound(currentStep - IO_SAMPLING_STEPS * 1.0);
writeChartXAxis.setUpperBound(currentStep);
writeChartYAxis.setUpperBound(allTimeMax);
}

View File

@@ -1,5 +1,6 @@
package org.cryptomator.ui.unlock;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
@@ -32,12 +33,24 @@ public class UnlockInvalidMountPointController implements FxController {
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
}
public boolean getMustExist() {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
return requirement == MountPointRequirement.EMPTY_MOUNT_POINT;
public boolean getNotExisting() {
return getMountPointRequirement() == MountPointRequirement.EMPTY_MOUNT_POINT;
}
}
public boolean getExisting() {
return getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT;
}
public boolean getDriveLetterOccupied() {
return getMountPointRequirement() == MountPointRequirement.UNUSED_ROOT_DIR;
}
private MountPointRequirement getMountPointRequirement() {
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
return requirement;
}
}

View File

@@ -2,6 +2,7 @@ package org.cryptomator.ui.unlock;
import com.google.common.base.Throwables;
import dagger.Lazy;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
@@ -79,9 +80,10 @@ public class UnlockWorkflow extends Task<Boolean> {
}
private void handleInvalidMountPoint(InvalidMountPointException impExc) {
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
var requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement();
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
assert requirement != MountPointRequirement.UNUSED_ROOT_DIR || SystemUtils.IS_OS_WINDOWS; //Not implemented anywhere, but on Windows
Throwable cause = impExc.getCause();
// TODO: apply https://openjdk.java.net/jeps/8213076 in future JDK versions
@@ -93,7 +95,11 @@ public class UnlockWorkflow extends Task<Boolean> {
}
showInvalidMountPointScene();
} else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
if (requirement == MountPointRequirement.UNUSED_ROOT_DIR) {
LOG.error("Unlock failed. Drive Letter already in use: {}", cause.getMessage());
} else {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
}
showInvalidMountPointScene();
} else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());