mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-22 20:51:27 +00:00
Merge branch 'develop' into feature/autoLock
This commit is contained in:
10
.github/workflows/build.yml
vendored
10
.github/workflows/build.yml
vendored
@@ -92,6 +92,16 @@ jobs:
|
||||
* [ ] add Linux appimage, zsync file and signature file
|
||||
* [ ] add Windows installer and signature file
|
||||
* [ ] add MacOs disk image and signature file
|
||||
|
||||
## What's new
|
||||
|
||||
## Bugfixes
|
||||
|
||||
## Misc
|
||||
|
||||
---
|
||||
|
||||
:scroll: A complete list of closed issues is available [here](LINK)
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload buildkit-linux.zip to GitHub Releases
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,5 +21,6 @@ pom.xml.versionsBackup
|
||||
.idea/compiler.xml
|
||||
.idea/encodings.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/**/libraries/
|
||||
*.iml
|
||||
@@ -48,7 +48,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
||||
|
||||
## Features
|
||||
|
||||
- Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||
- Works with Dropbox, Google Drive, OneDrive, MEGA, pCloud, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||
- Open Source means: No backdoors, control is better than trust
|
||||
- Client-side: No accounts, no data shared with any online service
|
||||
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.cryptomator.common.settings.SettingsProvider;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -25,6 +26,8 @@ import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -55,6 +58,22 @@ public abstract class CommonsModule {
|
||||
""";
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static SecureRandom provideCSPRNG() {
|
||||
try {
|
||||
return SecureRandom.getInstanceStrong();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
static MasterkeyFileAccess provideMasterkeyFileAccess(SecureRandom csprng) {
|
||||
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
|
||||
@@ -3,5 +3,8 @@ package org.cryptomator.common;
|
||||
public interface Constants {
|
||||
|
||||
String MASTERKEY_FILENAME = "masterkey.cryptomator";
|
||||
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
|
||||
String VAULTCONFIG_FILENAME = "vault.cryptomator";
|
||||
byte[] PEPPER = new byte[0];
|
||||
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class VaultSettings {
|
||||
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
|
||||
public static final boolean DEFAULT_USES_READONLY_MODE = false;
|
||||
public static final String DEFAULT_MOUNT_FLAGS = "";
|
||||
public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
|
||||
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
|
||||
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
|
||||
public static final boolean DEFAULT_LOCK_AFTER_TIME = false;
|
||||
public static final int DEFAULT_LOCK_TIME_IN_MINUTES = 30;
|
||||
@@ -45,7 +45,7 @@ public class VaultSettings {
|
||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
|
||||
private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
|
||||
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
||||
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
|
||||
private final BooleanProperty lockAfterTime = new SimpleBooleanProperty(DEFAULT_LOCK_AFTER_TIME);
|
||||
private final IntegerProperty lockTimeInMinutes = new SimpleIntegerProperty(DEFAULT_LOCK_TIME_IN_MINUTES);
|
||||
@@ -57,7 +57,7 @@ public class VaultSettings {
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock, lockAfterTime, lockTimeInMinutes};
|
||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, lockAfterTime, lockTimeInMinutes};
|
||||
}
|
||||
|
||||
public static VaultSettings withRandomId() {
|
||||
@@ -146,8 +146,8 @@ public class VaultSettings {
|
||||
return mountFlags;
|
||||
}
|
||||
|
||||
public IntegerProperty filenameLengthLimit() {
|
||||
return filenameLengthLimit;
|
||||
public IntegerProperty maxCleartextFilenameLength() {
|
||||
return maxCleartextFilenameLength;
|
||||
}
|
||||
|
||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||
|
||||
@@ -29,7 +29,7 @@ class VaultSettingsJsonAdapter {
|
||||
out.name("customMountPath").value(value.customMountPath().get());
|
||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
|
||||
out.name("mountFlags").value(value.mountFlags().get());
|
||||
out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
|
||||
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
|
||||
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
|
||||
out.name("lockAfterTime").value(value.lockAfterTime().get());
|
||||
out.name("lockTimeInMinutes").value(value.lockTimeInMinutes().get());
|
||||
@@ -48,7 +48,7 @@ class VaultSettingsJsonAdapter {
|
||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
|
||||
int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
|
||||
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
||||
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
|
||||
boolean lockAfterTime = VaultSettings.DEFAULT_LOCK_AFTER_TIME;
|
||||
int lockTimeInMinutes = VaultSettings.DEFAULT_LOCK_TIME_IN_MINUTES;
|
||||
@@ -68,7 +68,7 @@ class VaultSettingsJsonAdapter {
|
||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||
case "mountFlags" -> mountFlags = in.nextString();
|
||||
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
|
||||
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||
case "lockAfterTime" -> lockAfterTime = in.nextBoolean();
|
||||
case "lockTimeInMinutes" -> lockTimeInMinutes = in.nextInt();
|
||||
@@ -94,7 +94,7 @@ class VaultSettingsJsonAdapter {
|
||||
vaultSettings.customMountPath().set(customMountPath);
|
||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
|
||||
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||
vaultSettings.lockAfterTime().set(lockAfterTime);
|
||||
vaultSettings.lockTimeInMinutes().set(lockTimeInMinutes);
|
||||
|
||||
@@ -5,15 +5,15 @@ 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.dokany.DokanyMountFailedException;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
import org.cryptomator.frontend.dokany.MountFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
@@ -22,15 +22,13 @@ public class DokanyVolume extends AbstractVolume {
|
||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountFactory mountFactory;
|
||||
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.mountFactory = new MountFactory(executorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -39,11 +37,11 @@ public class DokanyVolume extends AbstractVolume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
try {
|
||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
|
||||
} catch (MountFailedException e) {
|
||||
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
||||
} catch (DokanyMountFailedException e) {
|
||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||
@@ -20,6 +20,7 @@ import javax.inject.Named;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
@@ -35,20 +36,21 @@ public class FuseVolume extends AbstractVolume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
|
||||
mount(fs.getPath("/"), mountFlags);
|
||||
mount(fs.getPath("/"), mountFlags, onExitAction);
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags) throws VolumeException {
|
||||
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
.withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
|
||||
.withFlags(splitFlags(mountFlags)) //
|
||||
.withMountPoint(mountPoint) //
|
||||
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
||||
.build();
|
||||
this.mount = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException | FuseNotSupportedException e) {
|
||||
this.mount = mounter.mount(root, envVars, onExitAction);
|
||||
} catch ( FuseMountException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
@@ -89,8 +91,7 @@ public class FuseVolume extends AbstractVolume {
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.unmountForced();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
@@ -100,8 +101,7 @@ public class FuseVolume extends AbstractVolume {
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public class LockNotCompletedException extends Exception {
|
||||
|
||||
public LockNotCompletedException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public LockNotCompletedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.common.Constants;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -35,28 +37,29 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@PerVault
|
||||
public class Vault {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
|
||||
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final Provider<Volume> volumeProvider;
|
||||
private final StringBinding defaultMountFlags;
|
||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||
private final ObjectProperty<VaultState> state;
|
||||
private final VaultState state;
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayName;
|
||||
@@ -74,7 +77,7 @@ public class Vault {
|
||||
private volatile Volume volume;
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
@@ -99,24 +102,31 @@ public class Vault {
|
||||
// Commands
|
||||
// ********************************************************************************/
|
||||
|
||||
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
||||
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
|
||||
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.add(FileSystemFlags.READONLY);
|
||||
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
|
||||
LOG.debug("Determining cleartext filename length limitations...");
|
||||
var checker = new FileSystemCapabilityChecker();
|
||||
int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold();
|
||||
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
|
||||
if (ciphertextLimit < shorteningThreshold) {
|
||||
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
|
||||
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
|
||||
} else {
|
||||
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
|
||||
}
|
||||
}
|
||||
if (!flags.contains(FileSystemFlags.READONLY) && vaultSettings.filenameLengthLimit().get() == -1) {
|
||||
LOG.debug("Determining file name length limitations...");
|
||||
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
|
||||
vaultSettings.filenameLengthLimit().set(limit);
|
||||
LOG.info("Storing file name length limit of {}", limit);
|
||||
|
||||
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
|
||||
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
|
||||
}
|
||||
assert vaultSettings.filenameLengthLimit().get() > 0;
|
||||
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withKeyLoader(keyLoader) //
|
||||
.withFlags(flags) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
|
||||
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
|
||||
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
@@ -133,29 +143,51 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() == null) {
|
||||
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
|
||||
cryptoFileSystem.set(fs);
|
||||
try {
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags());
|
||||
} catch (Exception e) {
|
||||
destroyCryptoFileSystem();
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() != null) {
|
||||
throw new IllegalStateException("Already unlocked.");
|
||||
}
|
||||
CryptoFileSystem fs = createCryptoFileSystem(keyLoader);
|
||||
boolean success = false;
|
||||
try {
|
||||
cryptoFileSystem.set(fs);
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
destroyCryptoFileSystem();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException {
|
||||
private void lockOnVolumeExit(Throwable t) {
|
||||
LOG.info("Unmounted vault '{}'", getDisplayName());
|
||||
destroyCryptoFileSystem();
|
||||
state.set(VaultState.Value.LOCKED);
|
||||
if (t != null) {
|
||||
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
||||
//initiate unmount
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
destroyCryptoFileSystem();
|
||||
|
||||
//wait for lockOnVolumeExit to be executed
|
||||
try {
|
||||
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
||||
if (!locked) {
|
||||
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LockNotCompletedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||
@@ -166,16 +198,12 @@ public class Vault {
|
||||
// Observable Properties
|
||||
// *******************************************************************************
|
||||
|
||||
public ObjectProperty<VaultState> stateProperty() {
|
||||
public VaultState stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public VaultState getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
public void setState(VaultState value) {
|
||||
state.setValue(value);
|
||||
public VaultState.Value getState() {
|
||||
return state.getValue();
|
||||
}
|
||||
|
||||
public ObjectProperty<Exception> lastKnownExceptionProperty() {
|
||||
@@ -195,7 +223,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return state.get() == VaultState.LOCKED;
|
||||
return state.get() == VaultState.Value.LOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding processingProperty() {
|
||||
@@ -203,7 +231,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isProcessing() {
|
||||
return state.get() == VaultState.PROCESSING;
|
||||
return state.get() == VaultState.Value.PROCESSING;
|
||||
}
|
||||
|
||||
public BooleanBinding unlockedProperty() {
|
||||
@@ -211,7 +239,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return state.get() == VaultState.UNLOCKED;
|
||||
return state.get() == VaultState.Value.UNLOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding missingProperty() {
|
||||
@@ -219,7 +247,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isMissing() {
|
||||
return state.get() == VaultState.MISSING;
|
||||
return state.get() == VaultState.Value.MISSING;
|
||||
}
|
||||
|
||||
public BooleanBinding needsMigrationProperty() {
|
||||
@@ -227,7 +255,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isNeedsMigration() {
|
||||
return state.get() == VaultState.NEEDS_MIGRATION;
|
||||
return state.get() == VaultState.Value.NEEDS_MIGRATION;
|
||||
}
|
||||
|
||||
public BooleanBinding unknownErrorProperty() {
|
||||
@@ -235,7 +263,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isUnknownError() {
|
||||
return state.get() == VaultState.ERROR;
|
||||
return state.get() == VaultState.Value.ERROR;
|
||||
}
|
||||
|
||||
public StringBinding displayNameProperty() {
|
||||
@@ -251,7 +279,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public String getAccessPoint() {
|
||||
if (state.get() == VaultState.UNLOCKED) {
|
||||
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
||||
assert volume != null;
|
||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
||||
} else {
|
||||
@@ -299,6 +327,12 @@ public class Vault {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
|
||||
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||
return VaultConfig.decode(token);
|
||||
}
|
||||
|
||||
public Observable[] observables() {
|
||||
return new Observable[]{state};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public interface VaultComponent {
|
||||
Builder vaultSettings(VaultSettings vaultSettings);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialVaultState(VaultState vaultState);
|
||||
Builder initialVaultState(VaultState.Value vaultState);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||
|
||||
@@ -11,6 +11,7 @@ package org.cryptomator.common.vaults;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.DirStructure;
|
||||
import org.cryptomator.cryptofs.migration.Migrators;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,14 +21,16 @@ import javax.inject.Singleton;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||
|
||||
@Singleton
|
||||
public class VaultListManager {
|
||||
@@ -52,19 +55,18 @@ public class VaultListManager {
|
||||
return vaultList;
|
||||
}
|
||||
|
||||
public Vault add(Path pathToVault) throws NoSuchFileException {
|
||||
public Vault add(Path pathToVault) throws IOException {
|
||||
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
||||
if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
|
||||
if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
|
||||
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
|
||||
}
|
||||
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
|
||||
if (alreadyExistingVault.isPresent()) {
|
||||
return alreadyExistingVault.get();
|
||||
} else {
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
}
|
||||
|
||||
return get(normalizedPathToVault) //
|
||||
.orElseGet(() -> {
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
});
|
||||
}
|
||||
|
||||
private VaultSettings newVaultSettings(Path path) {
|
||||
@@ -94,43 +96,45 @@ public class VaultListManager {
|
||||
private Vault create(VaultSettings vaultSettings) {
|
||||
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
||||
try {
|
||||
VaultState vaultState = determineVaultState(vaultSettings.path().get());
|
||||
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
|
||||
compBuilder.initialVaultState(vaultState);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
||||
compBuilder.initialVaultState(VaultState.ERROR);
|
||||
compBuilder.initialVaultState(ERROR);
|
||||
compBuilder.initialErrorCause(e);
|
||||
}
|
||||
return compBuilder.build().vault();
|
||||
}
|
||||
|
||||
public static VaultState redetermineVaultState(Vault vault) {
|
||||
VaultState previousState = vault.getState();
|
||||
public static VaultState.Value redetermineVaultState(Vault vault) {
|
||||
VaultState state = vault.stateProperty();
|
||||
VaultState.Value previousState = state.getValue();
|
||||
return switch (previousState) {
|
||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||
try {
|
||||
VaultState determinedState = determineVaultState(vault.getPath());
|
||||
vault.setState(determinedState);
|
||||
var determinedState = determineVaultState(vault.getPath());
|
||||
state.set(determinedState);
|
||||
yield determinedState;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
||||
vault.setState(VaultState.ERROR);
|
||||
state.set(ERROR);
|
||||
vault.setLastKnownException(e);
|
||||
yield VaultState.ERROR;
|
||||
yield ERROR;
|
||||
}
|
||||
}
|
||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||
};
|
||||
}
|
||||
|
||||
private static VaultState determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
||||
return VaultState.MISSING;
|
||||
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
||||
return VaultState.NEEDS_MIGRATION;
|
||||
} else {
|
||||
return VaultState.LOCKED;
|
||||
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!Files.exists(pathToVault)) {
|
||||
return VaultState.Value.MISSING;
|
||||
}
|
||||
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
|
||||
case VAULT -> VaultState.Value.LOCKED;
|
||||
case UNRELATED -> VaultState.Value.MISSING;
|
||||
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,12 +40,6 @@ public class VaultModule {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
|
||||
return new SimpleObjectProperty<>(initialState);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("lastKnownException")
|
||||
@PerVault
|
||||
@@ -53,7 +47,6 @@ public class VaultModule {
|
||||
return new SimpleObjectProperty<>(initialErrorCause);
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
@@ -105,7 +98,6 @@ public class VaultModule {
|
||||
flags.append(" -oatomic_o_trunc");
|
||||
flags.append(" -oauto_xattr");
|
||||
flags.append(" -oauto_cache");
|
||||
flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding
|
||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||
|
||||
|
||||
@@ -1,34 +1,141 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public enum VaultState {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.value.ObservableValueBase;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@PerVault
|
||||
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
|
||||
|
||||
public enum Value {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
|
||||
/**
|
||||
* Vault requires migration to a newer vault format
|
||||
*/
|
||||
NEEDS_MIGRATION,
|
||||
|
||||
/**
|
||||
* Vault ready to be unlocked
|
||||
*/
|
||||
LOCKED,
|
||||
|
||||
/**
|
||||
* Vault in transition between two other states
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Vault is unlocked
|
||||
*/
|
||||
UNLOCKED,
|
||||
|
||||
/**
|
||||
* Unknown state due to preceeding unrecoverable exceptions.
|
||||
*/
|
||||
ERROR;
|
||||
}
|
||||
|
||||
private final AtomicReference<Value> value;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Condition valueChanged = lock.newCondition();
|
||||
|
||||
@Inject
|
||||
public VaultState(VaultState.Value initialValue) {
|
||||
this.value = new AtomicReference<>(initialValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value get() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault requires migration to a newer vault format
|
||||
* Transitions from <code>fromState</code> to <code>toState</code>.
|
||||
*
|
||||
* @param fromState Previous state
|
||||
* @param toState New state
|
||||
* @return <code>true</code> if successful
|
||||
*/
|
||||
NEEDS_MIGRATION,
|
||||
public boolean transition(Value fromState, Value toState) {
|
||||
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
|
||||
boolean success = value.compareAndSet(fromState, toState);
|
||||
if (success) {
|
||||
fireValueChangedEvent();
|
||||
} else {
|
||||
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public void set(Value newState) {
|
||||
var oldState = value.getAndSet(newState);
|
||||
if (oldState != newState) {
|
||||
fireValueChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault ready to be unlocked
|
||||
* Waits for the specified time, until the desired state is reached.
|
||||
*
|
||||
* @param desiredState what state to wait for
|
||||
* @param time the maximum time to wait
|
||||
* @param unit the time unit of the {@code time} argument
|
||||
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
|
||||
* @throws InterruptedException if the current thread is interrupted
|
||||
*/
|
||||
LOCKED,
|
||||
public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
|
||||
while (value.get() != desiredState) {
|
||||
if (remaining <= 0L) {
|
||||
return false;
|
||||
}
|
||||
remaining = valueChanged.awaitNanos(remaining);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault in transition between two other states
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Vault is unlocked
|
||||
*/
|
||||
UNLOCKED,
|
||||
|
||||
/**
|
||||
* Unknown state due to preceeding unrecoverable exceptions.
|
||||
*/
|
||||
ERROR;
|
||||
private void signal() {
|
||||
lock.lock();
|
||||
try {
|
||||
valueChanged.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fireValueChangedEvent() {
|
||||
signal();
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
super.fireValueChangedEvent();
|
||||
} else {
|
||||
Platform.runLater(super::fireValueChangedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class VaultStats {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
||||
|
||||
private final AtomicReference<CryptoFileSystem> fs;
|
||||
private final ObjectProperty<VaultState> state;
|
||||
private final VaultState state;
|
||||
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||
@@ -41,7 +41,7 @@ public class VaultStats {
|
||||
private final LongProperty filesWritten = new SimpleLongProperty();
|
||||
|
||||
@Inject
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
|
||||
this.fs = fs;
|
||||
this.state = state;
|
||||
this.updateService = new UpdateStatsService();
|
||||
@@ -52,13 +52,13 @@ public class VaultStats {
|
||||
}
|
||||
|
||||
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
if (VaultState.UNLOCKED.equals(state.get())) {
|
||||
if (VaultState.Value.UNLOCKED == state.get()) {
|
||||
assert fs.get() != null;
|
||||
LOG.debug("start recording stats");
|
||||
updateService.restart();
|
||||
Platform.runLater(() -> updateService.restart());
|
||||
} else {
|
||||
LOG.debug("stop recording stats");
|
||||
updateService.cancel();
|
||||
Platform.runLater(() -> updateService.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -32,7 +34,7 @@ public interface Volume {
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
|
||||
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
||||
|
||||
/**
|
||||
* Reveals the mounted volume.
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
@@ -31,6 +32,7 @@ public class WebDavVolume implements Volume {
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
private Consumer<Throwable> onExitAction;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||
@@ -41,12 +43,13 @@ public class WebDavVolume implements Volume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
startServlet(fs);
|
||||
mountServlet();
|
||||
this.onExitAction = onExitAction;
|
||||
}
|
||||
|
||||
private void startServlet(CryptoFileSystem fs){
|
||||
private void startServlet(CryptoFileSystem fs) {
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
@@ -66,7 +69,7 @@ public class WebDavVolume implements Volume {
|
||||
|
||||
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
|
||||
Supplier<String> driveLetterSupplier;
|
||||
if(System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
|
||||
} else {
|
||||
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
|
||||
@@ -101,6 +104,7 @@ public class WebDavVolume implements Volume {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,6 +115,7 @@ public class WebDavVolume implements Volume {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
26
main/pom.xml
26
main/pom.xml
@@ -25,29 +25,29 @@
|
||||
<project.jdk.version>16</project.jdk.version>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>1.9.14</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptofs.version>2.1.0-beta5</cryptomator.cryptofs.version>
|
||||
<cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>1.2.9</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.2.4</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.1.4</cryptomator.webdav.version>
|
||||
<cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.2.2</cryptomator.webdav.version>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<javafx.version>15</javafx.version>
|
||||
<javafx.version>16</javafx.version>
|
||||
<commons-lang3.version>3.11</commons-lang3.version>
|
||||
<jwt.version>3.12.0</jwt.version>
|
||||
<jwt.version>3.15.0</jwt.version>
|
||||
<easybind.version>2.1.0</easybind.version>
|
||||
<guava.version>30.0-jre</guava.version>
|
||||
<dagger.version>2.32</dagger.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
<dagger.version>2.35.1</dagger.version>
|
||||
<gson.version>2.8.6</gson.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<logback.version>1.2.3</logback.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.7.0</junit.jupiter.version>
|
||||
<mockito.version>3.6.0</mockito.version>
|
||||
<junit.jupiter.version>5.7.1</junit.jupiter.version>
|
||||
<mockito.version>3.9.0</mockito.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
</properties>
|
||||
|
||||
@@ -218,12 +218,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- TODO: temporary fix for XXE attack, can be removed once java-jwt is updated -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.10.5.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
|
||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
|
||||
<suppress>
|
||||
<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
|
||||
<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
|
||||
<cve>CVE-2020-25649</cve>
|
||||
</suppress>
|
||||
<suppress>
|
||||
<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes>
|
||||
<gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav>
|
||||
|
||||
@@ -6,10 +6,10 @@ import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||
@@ -33,13 +33,6 @@ import java.util.ResourceBundle;
|
||||
@Module
|
||||
public abstract class AddVaultModule {
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardScoped
|
||||
@Named("newPassword")
|
||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
||||
return new SimpleObjectProperty<>("");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@AddVaultWizardWindow
|
||||
@AddVaultWizardScoped
|
||||
@@ -167,8 +160,8 @@ public abstract class AddVaultModule {
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
@Binds
|
||||
|
||||
@@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
Vault newVault = vaultListManager.add(vaultPath.get());
|
||||
vault.set(newVault);
|
||||
window.setScene(successScene.get());
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to open existing vault.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -15,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -43,55 +43,74 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseNameScene;
|
||||
private final Lazy<Scene> choosePasswordScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final LocationPresets locationPresets;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final BooleanBinding validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final StringProperty warningText;
|
||||
private final StringProperty statusText;
|
||||
private final ObjectProperty<Node> statusGraphic;
|
||||
|
||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||
|
||||
//FXML
|
||||
public ToggleGroup predefinedLocationToggler;
|
||||
public RadioButton iclouddriveRadioButton;
|
||||
public RadioButton dropboxRadioButton;
|
||||
public RadioButton gdriveRadioButton;
|
||||
public RadioButton onedriveRadioButton;
|
||||
public RadioButton megaRadioButton;
|
||||
public RadioButton pcloudRadioButton;
|
||||
public RadioButton customRadioButton;
|
||||
public Label vaultPathStatus;
|
||||
public FontAwesome5IconView goodLocation;
|
||||
public FontAwesome5IconView badLocation;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ErrorComponent.Builder errorComponent, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.chooseNameScene = chooseNameScene;
|
||||
this.choosePasswordScene = choosePasswordScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.locationPresets = locationPresets;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vaultName = vaultName;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
|
||||
this.usePresetPath = new SimpleBooleanProperty();
|
||||
this.warningText = new SimpleStringProperty();
|
||||
this.statusText = new SimpleStringProperty();
|
||||
this.statusGraphic = new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private boolean isValidVaultPath() {
|
||||
return vaultPath.get() != null && Files.notExists(vaultPath.get());
|
||||
private boolean validateVaultPathAndSetStatus() {
|
||||
final Path p = vaultPath.get();
|
||||
if (p == null) {
|
||||
statusText.set("Error: Path is NULL.");
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.exists(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.isWritable(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.notExists(p)) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
|
||||
statusGraphic.set(goodLocation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||
vaultPath.addListener(this::vaultPathDidChange);
|
||||
}
|
||||
|
||||
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
|
||||
if (!Files.notExists(newValue)) {
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
} else {
|
||||
warningText.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||
@@ -103,6 +122,10 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
||||
} else if (onedriveRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
|
||||
} else if (megaRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
|
||||
} else if (pcloudRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
|
||||
} else if (customRadioButton.equals(newValue)) {
|
||||
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||
}
|
||||
@@ -115,21 +138,10 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
try {
|
||||
// check if we have write access AND the vaultPath doesn't already exist:
|
||||
assert Files.isDirectory(vaultPath.get().getParent());
|
||||
Path createdDir = Files.createDirectory(vaultPath.get());
|
||||
Files.delete(createdDir); // assert: dir exists and is empty
|
||||
if (validateVaultPathAndSetStatus()) {
|
||||
window.setScene(choosePasswordScene.get());
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
} catch (NoSuchFileException e) {
|
||||
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create and delete directory at chosen vault path.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
} else {
|
||||
validVaultPath.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,19 +191,27 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
return usePresetPath.get();
|
||||
}
|
||||
|
||||
public StringProperty warningTextProperty() {
|
||||
return warningText;
|
||||
public BooleanBinding anyRadioButtonSelectedProperty() {
|
||||
return predefinedLocationToggler.selectedToggleProperty().isNotNull();
|
||||
}
|
||||
|
||||
public String getWarningText() {
|
||||
return warningText.get();
|
||||
public boolean isAnyRadioButtonSelected() {
|
||||
return anyRadioButtonSelectedProperty().get();
|
||||
}
|
||||
|
||||
public BooleanBinding showWarningProperty() {
|
||||
return warningText.isNotEmpty();
|
||||
public StringProperty statusTextProperty() {
|
||||
return statusText;
|
||||
}
|
||||
|
||||
public boolean isShowWarning() {
|
||||
return showWarningProperty().get();
|
||||
public String getStatusText() {
|
||||
return statusText.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> statusGraphicProperty() {
|
||||
return statusGraphic;
|
||||
}
|
||||
|
||||
public Node getStatusGraphic() {
|
||||
return statusGraphic.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,18 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultCipherCombo;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.Tasks;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -17,7 +24,6 @@ import org.slf4j.LoggerFactory;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -31,13 +37,13 @@ import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Collections;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@@ -48,6 +54,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
public class CreateNewVaultPasswordController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
||||
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseLocationScene;
|
||||
@@ -62,8 +69,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
private final StringProperty recoveryKeyProperty;
|
||||
private final VaultListManager vaultListManager;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final ObjectProperty<CharSequence> password;
|
||||
private final ReadmeGenerator readmeGenerator;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
private final BooleanProperty processing;
|
||||
private final BooleanProperty readyToCreateVault;
|
||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||
@@ -71,9 +79,10 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
public ToggleGroup recoveryKeyChoice;
|
||||
public Toggle showRecoveryKey;
|
||||
public Toggle skipRecoveryKey;
|
||||
public NewPasswordController newPasswordSceneController;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
|
||||
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.chooseLocationScene = chooseLocationScene;
|
||||
this.recoveryKeyScene = recoveryKeyScene;
|
||||
@@ -87,8 +96,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
this.recoveryKeyProperty = recoveryKey;
|
||||
this.vaultListManager = vaultListManager;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.password = password;
|
||||
this.readmeGenerator = readmeGenerator;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
this.processing = new SimpleBooleanProperty();
|
||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||
@@ -96,8 +106,11 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
|
||||
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
||||
readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
||||
window.setOnHiding(event -> {
|
||||
newPasswordSceneController.passwordField.wipe();
|
||||
newPasswordSceneController.reenterField.wipe();
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -130,8 +143,8 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
Path pathToVault = vaultPathProperty.get();
|
||||
processing.set(true);
|
||||
Tasks.create(() -> {
|
||||
initializeVault(pathToVault, password.get());
|
||||
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
|
||||
initializeVault(pathToVault);
|
||||
return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters());
|
||||
}).onSuccess(recoveryKey -> {
|
||||
initializationSucceeded(pathToVault);
|
||||
recoveryKeyProperty.set(recoveryKey);
|
||||
@@ -148,7 +161,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
Path pathToVault = vaultPathProperty.get();
|
||||
processing.set(true);
|
||||
Tasks.create(() -> {
|
||||
initializeVault(pathToVault, password.get());
|
||||
initializeVault(pathToVault);
|
||||
}).onSuccess(() -> {
|
||||
initializationSucceeded(pathToVault);
|
||||
window.setScene(successScene.get());
|
||||
@@ -160,24 +173,35 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
}).runOnce(executor);
|
||||
}
|
||||
|
||||
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
||||
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withFlags(Collections.emptySet()) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.build();
|
||||
private void initializeVault(Path path) throws IOException {
|
||||
// 1. write masterkey:
|
||||
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||
try (Masterkey masterkey = Masterkey.generate(csprng)) {
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters());
|
||||
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
// 2. initialize vault:
|
||||
try {
|
||||
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||
|
||||
// 3. write vault-internal readme file:
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
throw new IOException("Failed initialize vault.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. write vault-external readme file:
|
||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||
}
|
||||
|
||||
LOG.info("Created vault at {}", path);
|
||||
}
|
||||
|
||||
@@ -185,7 +209,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
try {
|
||||
Vault newVault = vaultListManager.add(pathToVault);
|
||||
vaultProperty.set(newVault);
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,21 @@ public class LocationPresets {
|
||||
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
||||
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
|
||||
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
||||
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
|
||||
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
|
||||
|
||||
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> megaLocation;
|
||||
private final ReadOnlyObjectProperty<Path> pcloudLocation;
|
||||
private final BooleanBinding foundIclouddrive;
|
||||
private final BooleanBinding foundDropbox;
|
||||
private final BooleanBinding foundGdrive;
|
||||
private final BooleanBinding foundOnedrive;
|
||||
private final BooleanBinding foundMega;
|
||||
private final BooleanBinding foundPcloud;
|
||||
|
||||
@Inject
|
||||
public LocationPresets() {
|
||||
@@ -32,10 +38,14 @@ public class LocationPresets {
|
||||
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
||||
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
||||
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
|
||||
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
|
||||
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
|
||||
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
||||
this.foundDropbox = dropboxLocation.isNotNull();
|
||||
this.foundGdrive = gdriveLocation.isNotNull();
|
||||
this.foundOnedrive = onedriveLocation.isNotNull();
|
||||
this.foundMega = megaLocation.isNotNull();
|
||||
this.foundPcloud = pcloudLocation.isNotNull();
|
||||
}
|
||||
|
||||
private static Path existingWritablePath(String... candidates) {
|
||||
@@ -122,4 +132,36 @@ public class LocationPresets {
|
||||
return foundOnedrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
|
||||
return megaLocation;
|
||||
}
|
||||
|
||||
public Path getMegaLocation() {
|
||||
return megaLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundMegaProperty() {
|
||||
return foundMega;
|
||||
}
|
||||
|
||||
public boolean isFoundMega() {
|
||||
return foundMega.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
|
||||
return pcloudLocation;
|
||||
}
|
||||
|
||||
public Path getPcloudLocation() {
|
||||
return pcloudLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundPcloudProperty() {
|
||||
return foundPcloud;
|
||||
}
|
||||
|
||||
public boolean isFoundPcloud() {
|
||||
return foundPcloud.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,29 +2,33 @@ package org.cryptomator.ui.changepassword;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Optional;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@ChangePasswordScoped
|
||||
@@ -34,29 +38,36 @@ public class ChangePasswordController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final ObjectProperty<CharSequence> newPassword;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final KeychainManager keychain;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
|
||||
public NiceSecurePasswordField oldPasswordField;
|
||||
public CheckBox finalConfirmationCheckbox;
|
||||
public Button finishButton;
|
||||
public NewPasswordController newPasswordController;
|
||||
|
||||
@Inject
|
||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
|
||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.newPassword = newPassword;
|
||||
this.errorComponent = errorComponent;
|
||||
this.keychain = keychain;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
||||
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
||||
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
|
||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
|
||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
|
||||
window.setOnHiding(event -> {
|
||||
oldPasswordField.wipe();
|
||||
newPasswordController.passwordField.wipe();
|
||||
newPasswordController.reenterField.wipe();
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -67,24 +78,31 @@ public class ChangePasswordController implements FxController {
|
||||
@FXML
|
||||
public void finish() {
|
||||
try {
|
||||
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
|
||||
CharSequence oldPassphrase = oldPasswordField.getCharacters();
|
||||
CharSequence newPassphrase = newPasswordController.passwordField.getCharacters();
|
||||
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||
window.close();
|
||||
updatePasswordInSystemkeychain();
|
||||
} catch (IOException e) {
|
||||
LOG.error("IO error occured during password change. Unable to perform operation.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
window.close();
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
oldPasswordField.selectAll();
|
||||
oldPasswordField.requestFocus();
|
||||
} catch (IOException | CryptoException e) {
|
||||
LOG.error("Password change failed. Unable to perform operation.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePasswordInSystemkeychain() {
|
||||
if (keychain.isSupported()) {
|
||||
if (keychain.isSupported() && !keychain.isLocked()) {
|
||||
try {
|
||||
keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
|
||||
keychain.changePassphrase(vault.getId(), 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);
|
||||
|
||||
@@ -5,10 +5,10 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||
@@ -16,8 +16,6 @@ import org.cryptomator.ui.common.StageFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
@@ -27,13 +25,6 @@ import java.util.ResourceBundle;
|
||||
@Module
|
||||
abstract class ChangePasswordModule {
|
||||
|
||||
@Provides
|
||||
@ChangePasswordScoped
|
||||
@Named("newPassword")
|
||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
||||
return new SimpleObjectProperty<>("");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ChangePasswordWindow
|
||||
@ChangePasswordScoped
|
||||
@@ -71,8 +62,8 @@ abstract class ChangePasswordModule {
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ public enum FxmlFile {
|
||||
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||
ERROR("/fxml/error.fxml"), //
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
HEALTH_START("/fxml/health_start.fxml"), //
|
||||
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||
@@ -26,8 +28,9 @@ public enum FxmlFile {
|
||||
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
||||
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
||||
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||
UNLOCK("/fxml/unlock.fxml"),
|
||||
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
|
||||
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
|
||||
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
|
||||
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||
|
||||
@@ -8,7 +8,8 @@ import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -18,8 +19,8 @@ public class NewPasswordController implements FxController {
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final PasswordStrengthUtil strengthRater;
|
||||
private final ObjectProperty<CharSequence> password;
|
||||
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
||||
private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
|
||||
|
||||
public NiceSecurePasswordField passwordField;
|
||||
public NiceSecurePasswordField reenterField;
|
||||
@@ -31,10 +32,9 @@ public class NewPasswordController implements FxController {
|
||||
public FontAwesome5IconView passwordMatchCheckmark;
|
||||
public FontAwesome5IconView passwordMatchCross;
|
||||
|
||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
|
||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.strengthRater = strengthRater;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -44,7 +44,7 @@ public class NewPasswordController implements FxController {
|
||||
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||
|
||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
|
||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
|
||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
||||
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
||||
@@ -54,6 +54,7 @@ public class NewPasswordController implements FxController {
|
||||
reenterField.textProperty().addListener(this::passwordsDidChange);
|
||||
}
|
||||
|
||||
|
||||
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
||||
if (passwordField.getCharacters().length() == 0) {
|
||||
return null;
|
||||
@@ -67,17 +68,19 @@ public class NewPasswordController implements FxController {
|
||||
}
|
||||
|
||||
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
||||
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||
password.set(passwordField.getCharacters());
|
||||
} else {
|
||||
password.set("");
|
||||
if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||
passwordsMatchAndSufficient.setValue(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasSamePasswordInBothFields() {
|
||||
private boolean passwordFieldsMatch() {
|
||||
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
||||
}
|
||||
|
||||
public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
|
||||
return passwordsMatchAndSufficient.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public IntegerProperty passwordStrengthProperty() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
@@ -175,24 +176,29 @@ public class VaultService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException {
|
||||
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
||||
vault.lock(forced);
|
||||
return vault;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ package org.cryptomator.ui.controls;
|
||||
public enum FontAwesome5Icon {
|
||||
ANCHOR("\uF13D"), //
|
||||
ARROW_UP("\uF062"), //
|
||||
BAN("\uF05E"), //
|
||||
BUG("\uF188"), //
|
||||
CHECK("\uF00C"), //
|
||||
CLOCK("\uF017"), //
|
||||
COG("\uF013"), //
|
||||
COGS("\uF085"), //
|
||||
COPY("\uF0C5"), //
|
||||
|
||||
@@ -12,6 +12,7 @@ public class FormattedLabel extends Label {
|
||||
|
||||
private final StringProperty format = new SimpleStringProperty("");
|
||||
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<Object> arg2 = new SimpleObjectProperty<>();
|
||||
// add arg2, arg3, ... on demand
|
||||
|
||||
public FormattedLabel() {
|
||||
@@ -19,11 +20,11 @@ public class FormattedLabel extends Label {
|
||||
}
|
||||
|
||||
protected StringBinding createStringBinding() {
|
||||
return Bindings.createStringBinding(this::updateText, format, arg1);
|
||||
return Bindings.createStringBinding(this::updateText, format, arg1, arg2);
|
||||
}
|
||||
|
||||
private String updateText() {
|
||||
return String.format(format.get(), arg1.get());
|
||||
return String.format(format.get(), arg1.get(), arg2.get());
|
||||
}
|
||||
|
||||
/* Observables */
|
||||
@@ -51,4 +52,16 @@ public class FormattedLabel extends Label {
|
||||
public void setArg1(Object arg1) {
|
||||
this.arg1.set(arg1);
|
||||
}
|
||||
|
||||
public ObjectProperty<Object> arg2Property() {
|
||||
return arg2;
|
||||
}
|
||||
|
||||
public Object getArg2() {
|
||||
return arg2.get();
|
||||
}
|
||||
|
||||
public void setArg2(Object arg2) {
|
||||
this.arg2.set(arg2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ public class SecurePasswordField extends TextField {
|
||||
}
|
||||
|
||||
private void updateCapsLocked() {
|
||||
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
||||
//TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680)
|
||||
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,13 @@ import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.UiTheme;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
||||
import org.cryptomator.integrations.uiappearance.Theme;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
@@ -47,8 +49,9 @@ public class FxApplication extends Application {
|
||||
private final Lazy<MainWindowComponent> mainWindow;
|
||||
private final Lazy<PreferencesComponent> preferencesWindow;
|
||||
private final Lazy<QuitComponent> quitWindow;
|
||||
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
|
||||
private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
|
||||
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
|
||||
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
|
||||
private final ErrorComponent.Builder errorWindowBuilder;
|
||||
private final Optional<TrayIntegrationProvider> trayIntegration;
|
||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||
private final VaultService vaultService;
|
||||
@@ -60,13 +63,14 @@ public class FxApplication extends Application {
|
||||
private final ScheduledExecutorService scheduledExecutorService;
|
||||
|
||||
@Inject
|
||||
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) {
|
||||
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) {
|
||||
this.settings = settings;
|
||||
this.mainWindow = mainWindow;
|
||||
this.preferencesWindow = preferencesWindow;
|
||||
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
|
||||
this.lockWindowBuilderProvider = lockWindowBuilderProvider;
|
||||
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
|
||||
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
|
||||
this.quitWindow = quitWindow;
|
||||
this.errorWindowBuilder = errorWindowBuilder;
|
||||
this.trayIntegration = trayIntegration;
|
||||
this.appearanceProvider = appearanceProvider;
|
||||
this.vaultService = vaultService;
|
||||
@@ -93,7 +97,7 @@ public class FxApplication extends Application {
|
||||
}
|
||||
|
||||
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
|
||||
LOG.warn("has visible stages: {}", newValue);
|
||||
LOG.debug("has visible stages: {}", newValue);
|
||||
if (newValue) {
|
||||
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
|
||||
} else {
|
||||
@@ -120,16 +124,24 @@ public class FxApplication extends Application {
|
||||
|
||||
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||
Platform.runLater(() -> {
|
||||
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
||||
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
|
||||
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
|
||||
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
||||
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
|
||||
} else {
|
||||
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
|
||||
}
|
||||
});
|
||||
checkAutolock(vault, owner);
|
||||
}
|
||||
|
||||
public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||
Platform.runLater(() -> {
|
||||
lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
|
||||
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
|
||||
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
|
||||
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
|
||||
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
|
||||
} else {
|
||||
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -39,8 +40,13 @@ public abstract class UpdateCheckerModule {
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static HttpClient provideHttpClient() {
|
||||
return HttpClient.newHttpClient();
|
||||
static Optional<HttpClient> provideHttpClient() {
|
||||
try {
|
||||
return Optional.of(HttpClient.newHttpClient());
|
||||
} catch (UncheckedIOException e) {
|
||||
LOG.error("HttpClient for update check cannot be created.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -66,11 +72,20 @@ public abstract class UpdateCheckerModule {
|
||||
|
||||
@Provides
|
||||
@FxApplicationScoped
|
||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
|
||||
ScheduledService<String> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new UpdateCheckerTask(httpClient, checkForUpdatesRequest);
|
||||
if (httpClient.isPresent()) {
|
||||
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
|
||||
} else {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() {
|
||||
throw new NullPointerException("No HttpClient present.");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Suppliers;
|
||||
import dagger.Lazy;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class BatchService extends Service<Void> {
|
||||
|
||||
private final Iterator<HealthCheckTask> remainingTasks;
|
||||
|
||||
@Inject
|
||||
public BatchService(Iterable<HealthCheckTask> tasks) {
|
||||
this.remainingTasks = tasks.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
|
||||
return remainingTasks.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
if (remainingTasks.hasNext()) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.EasyObservableList;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class CheckDetailController implements FxController {
|
||||
|
||||
private final EasyObservableList<DiagnosticResult> results;
|
||||
private final OptionalBinding<Worker.State> taskState;
|
||||
private final Binding<String> taskName;
|
||||
private final Binding<Number> taskDuration;
|
||||
private final ResultListCellFactory resultListCellFactory;
|
||||
private final Binding<Boolean> taskRunning;
|
||||
private final Binding<Boolean> taskScheduled;
|
||||
private final Binding<Boolean> taskFinished;
|
||||
private final Binding<Boolean> taskNotStarted;
|
||||
private final Binding<Boolean> taskSucceeded;
|
||||
private final Binding<Boolean> taskFailed;
|
||||
private final Binding<Boolean> taskCancelled;
|
||||
private final Binding<Number> countOfWarnSeverity;
|
||||
private final Binding<Number> countOfCritSeverity;
|
||||
|
||||
public ListView<DiagnosticResult> resultsListView;
|
||||
private Subscription resultSubscription;
|
||||
|
||||
@Inject
|
||||
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory) {
|
||||
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
|
||||
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
|
||||
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L);
|
||||
this.resultListCellFactory = resultListCellFactory;
|
||||
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
|
||||
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
|
||||
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
|
||||
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
|
||||
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
|
||||
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
|
||||
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
|
||||
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
|
||||
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
|
||||
selectedTask.addListener(this::selectedTaskChanged);
|
||||
}
|
||||
|
||||
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
|
||||
if (resultSubscription != null) {
|
||||
resultSubscription.unsubscribe();
|
||||
}
|
||||
if (newValue != null) {
|
||||
resultSubscription = EasyBind.bindContent(results, newValue.results());
|
||||
}
|
||||
}
|
||||
|
||||
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
|
||||
return stream -> stream.filter(item -> severity.equals(item.getServerity())).count();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
resultsListView.setItems(results);
|
||||
resultsListView.setCellFactory(resultListCellFactory);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getTaskName() {
|
||||
return taskName.getValue();
|
||||
}
|
||||
|
||||
public Binding<String> taskNameProperty() {
|
||||
return taskName;
|
||||
}
|
||||
|
||||
public Number getTaskDuration() {
|
||||
return taskDuration.getValue();
|
||||
}
|
||||
|
||||
public Binding<Number> taskDurationProperty() {
|
||||
return taskDuration;
|
||||
}
|
||||
|
||||
public long getCountOfWarnSeverity() {
|
||||
return countOfWarnSeverity.getValue().longValue();
|
||||
}
|
||||
|
||||
public Binding<Number> countOfWarnSeverityProperty() {
|
||||
return countOfWarnSeverity;
|
||||
}
|
||||
|
||||
public long getCountOfCritSeverity() {
|
||||
return countOfCritSeverity.getValue().longValue();
|
||||
}
|
||||
|
||||
public Binding<Number> countOfCritSeverityProperty() {
|
||||
return countOfCritSeverity;
|
||||
}
|
||||
|
||||
public boolean isTaskRunning() {
|
||||
return taskRunning.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskRunningProperty() {
|
||||
return taskRunning;
|
||||
}
|
||||
|
||||
public boolean isTaskFinished() {
|
||||
return taskFinished.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskFinishedProperty() {
|
||||
return taskFinished;
|
||||
}
|
||||
|
||||
public boolean isTaskScheduled() {
|
||||
return taskScheduled.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskScheduledProperty() {
|
||||
return taskScheduled;
|
||||
}
|
||||
|
||||
public boolean isTaskNotStarted() {
|
||||
return taskNotStarted.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskNotStartedProperty() {
|
||||
return taskNotStarted;
|
||||
}
|
||||
|
||||
public boolean isTaskSucceeded() {
|
||||
return taskSucceeded.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskSucceededProperty() {
|
||||
return taskSucceeded;
|
||||
}
|
||||
|
||||
public boolean isTaskFailed() {
|
||||
return taskFailed.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskFailedProperty() {
|
||||
return taskFailed;
|
||||
}
|
||||
|
||||
public boolean isTaskCancelled() {
|
||||
return taskCancelled.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskCancelledProperty() {
|
||||
return taskCancelled;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ListCell;
|
||||
|
||||
class CheckListCell extends ListCell<HealthCheckTask> {
|
||||
|
||||
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
|
||||
|
||||
CheckListCell() {
|
||||
setPadding(new Insets(6));
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(HealthCheckTask item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null) {
|
||||
textProperty().bind(item.titleProperty());
|
||||
item.stateProperty().addListener(this::stateChanged);
|
||||
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty()));
|
||||
stateIcon.setGlyph(glyphForState(item.getState()));
|
||||
} else {
|
||||
textProperty().unbind();
|
||||
graphicProperty().unbind();
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
|
||||
stateIcon.setGlyph(glyphForState(newState));
|
||||
stateIcon.setVisible(true);
|
||||
}
|
||||
|
||||
private Node graphicForState(Worker.State state) {
|
||||
return switch (state) {
|
||||
case READY -> null;
|
||||
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
|
||||
};
|
||||
}
|
||||
|
||||
private FontAwesome5Icon glyphForState(Worker.State state) {
|
||||
return switch (state) {
|
||||
case READY -> FontAwesome5Icon.COG; //just a placeholder
|
||||
case SCHEDULED -> FontAwesome5Icon.CLOCK;
|
||||
case RUNNING -> FontAwesome5Icon.SPINNER;
|
||||
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||
case CANCELLED -> FontAwesome5Icon.BAN;
|
||||
case SUCCEEDED -> FontAwesome5Icon.CHECK;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.cell.CheckBoxListCell;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class CheckListController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
|
||||
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
|
||||
|
||||
private final Stage window;
|
||||
private final ObservableList<HealthCheckTask> tasks;
|
||||
private final ReportWriter reportWriter;
|
||||
private final ExecutorService executorService;
|
||||
private final ObjectProperty<HealthCheckTask> selectedTask;
|
||||
private final Lazy<ErrorComponent.Builder> errorComponenBuilder;
|
||||
private final SimpleObjectProperty<Worker<?>> runningTask;
|
||||
private final Binding<Boolean> running;
|
||||
private final Binding<Boolean> finished;
|
||||
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
|
||||
private final IntegerProperty numberOfPickedChecks;
|
||||
private final BooleanBinding anyCheckSelected;
|
||||
private final BooleanProperty showResultScreen;
|
||||
|
||||
/* FXML */
|
||||
public ListView<HealthCheckTask> checksListView;
|
||||
|
||||
|
||||
@Inject
|
||||
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponenBuilder) {
|
||||
this.window = window;
|
||||
this.tasks = FXCollections.observableArrayList(tasks.get());
|
||||
this.reportWriter = reportWriteTask;
|
||||
this.executorService = executorService;
|
||||
this.selectedTask = selectedTask;
|
||||
this.errorComponenBuilder = errorComponenBuilder;
|
||||
this.runningTask = new SimpleObjectProperty<>();
|
||||
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
|
||||
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
|
||||
this.listPickIndicators = new HashMap<>();
|
||||
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
|
||||
this.tasks.forEach(task -> {
|
||||
var entrySelectedProp = new SimpleBooleanProperty(false);
|
||||
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
|
||||
listPickIndicators.put(task, entrySelectedProp);
|
||||
});
|
||||
this.anyCheckSelected = selectedTask.isNotNull();
|
||||
this.showResultScreen = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
checksListView.setItems(tasks);
|
||||
checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter<HealthCheckTask>() {
|
||||
@Override
|
||||
public String toString(HealthCheckTask object) {
|
||||
return object.getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HealthCheckTask fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
}));
|
||||
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void toggleSelectAll(ActionEvent event) {
|
||||
if (event.getSource() instanceof CheckBox c) {
|
||||
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void runSelectedChecks() {
|
||||
Preconditions.checkState(runningTask.get() == null);
|
||||
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
|
||||
var batchService = new BatchService(batch);
|
||||
batchService.setExecutor(executorService);
|
||||
batchService.start();
|
||||
runningTask.set(batchService);
|
||||
showResultScreen.set(true);
|
||||
checksListView.getSelectionModel().select(batch.get(0));
|
||||
checksListView.setCellFactory(view -> new CheckListCell());
|
||||
window.sizeToScene();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public synchronized void cancelCheck() {
|
||||
Preconditions.checkState(runningTask.get() != null);
|
||||
runningTask.get().cancel();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void exportResults() {
|
||||
try {
|
||||
reportWriter.writeReport(tasks);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to write health check report.", e);
|
||||
errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
public boolean isRunning() {
|
||||
return running.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> runningProperty() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> finishedProperty() {
|
||||
return finished;
|
||||
}
|
||||
|
||||
public boolean isAnyCheckSelected() {
|
||||
return anyCheckSelected.get();
|
||||
}
|
||||
|
||||
public BooleanBinding anyCheckSelectedProperty() {
|
||||
return anyCheckSelected;
|
||||
}
|
||||
|
||||
public boolean getShowResultScreen() {
|
||||
return showResultScreen.get();
|
||||
}
|
||||
|
||||
public BooleanProperty showResultScreenProperty() {
|
||||
return showResultScreen;
|
||||
}
|
||||
|
||||
public int getNumberOfPickedChecks() {
|
||||
return numberOfPickedChecks.get();
|
||||
}
|
||||
|
||||
public IntegerProperty numberOfPickedChecksProperty() {
|
||||
return numberOfPickedChecks;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@HealthCheckScoped
|
||||
@Subcomponent(modules = {HealthCheckModule.class})
|
||||
public interface HealthCheckComponent {
|
||||
|
||||
@HealthCheckWindow
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.HEALTH_START)
|
||||
Lazy<Scene> scene();
|
||||
|
||||
default Stage showHealthCheckWindow() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
stage.show();
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@HealthCheckWindow Vault vault);
|
||||
|
||||
HealthCheckComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||
abstract class HealthCheckModule {
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static AtomicReference<Masterkey> provideMasterkeyRef() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static AtomicReference<VaultConfig> provideVaultConfigRef() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static Collection<HealthCheck> provideAvailableHealthChecks() {
|
||||
return HealthCheck.allChecks();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
/* Only inject with Lazy-Wrapper!*/
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
|
||||
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
|
||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
|
||||
Stage stage = factory.create();
|
||||
stage.setTitle(resourceBundle.getString("health.title"));
|
||||
stage.setResizable(true);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(owner);
|
||||
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static ChangeListener<Boolean> provideWindowShowingChangeListener(AtomicReference<Masterkey> masterkey) {
|
||||
return (observable, wasShowing, isShowing) -> {
|
||||
if (!isShowing) {
|
||||
Optional.ofNullable(masterkey.getAndSet(null)).ifPresent(Masterkey::destroy);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HEALTH_START)
|
||||
@HealthCheckScoped
|
||||
static Scene provideHealthStartScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
|
||||
@HealthCheckScoped
|
||||
static Scene provideHealthCheckListScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HEALTH_CHECK_LIST);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(StartController.class)
|
||||
abstract FxController bindStartController(StartController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CheckListController.class)
|
||||
abstract FxController bindCheckController(CheckListController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CheckDetailController.class)
|
||||
abstract FxController bindCheckDetailController(CheckDetailController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ResultListCellController.class)
|
||||
abstract FxController bindResultListCellController(ResultListCellController controller);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface HealthCheckScoped {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
class HealthCheckTask extends Task<Void> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
|
||||
|
||||
private final Path vaultPath;
|
||||
private final VaultConfig vaultConfig;
|
||||
private final Masterkey masterkey;
|
||||
private final SecureRandom csprng;
|
||||
private final HealthCheck check;
|
||||
private final ObservableList<DiagnosticResult> results;
|
||||
private final LongProperty durationInMillis;
|
||||
|
||||
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
|
||||
this.vaultPath = Objects.requireNonNull(vaultPath);
|
||||
this.vaultConfig = Objects.requireNonNull(vaultConfig);
|
||||
this.masterkey = Objects.requireNonNull(masterkey);
|
||||
this.csprng = Objects.requireNonNull(csprng);
|
||||
this.check = Objects.requireNonNull(check);
|
||||
this.results = FXCollections.observableArrayList();
|
||||
try {
|
||||
updateTitle(resourceBundle.getString("health." + check.identifier()));
|
||||
} catch (MissingResourceException e) {
|
||||
LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier());
|
||||
updateTitle(check.identifier());
|
||||
}
|
||||
this.durationInMillis = new SimpleLongProperty(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
Instant start = Instant.now();
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
|
||||
if (isCancelled()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
// FIXME: slowdown for demonstration purposes only:
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
if (isCancelled()) {
|
||||
return;
|
||||
} else {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
Platform.runLater(() -> results.add(result));
|
||||
});
|
||||
}
|
||||
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
LOG.info("starting {}", check.identifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
LOG.info("finished {}", check.identifier());
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public ObservableList<DiagnosticResult> results() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public HealthCheck getCheck() {
|
||||
return check;
|
||||
}
|
||||
|
||||
public LongProperty durationInMillisProperty() {
|
||||
return durationInMillis;
|
||||
}
|
||||
|
||||
public long getDurationInMillis() {
|
||||
return durationInMillis.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
@interface HealthCheckWindow {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.concurrent.Worker;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class ReportWriter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
|
||||
private static final String REPORT_HEADER = """
|
||||
**************************************
|
||||
* Cryptomator Vault Health Report *
|
||||
**************************************
|
||||
Analyzed vault: %s (Current name "%s")
|
||||
Vault storage path: %s
|
||||
""";
|
||||
private static final String REPORT_CHECK_HEADER = """
|
||||
|
||||
|
||||
Check %s
|
||||
------------------------------
|
||||
""";
|
||||
private static final String REPORT_CHECK_RESULT = "%8s - %s\n";
|
||||
private static final DateTimeFormatter TIME_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault());
|
||||
|
||||
private final Vault vault;
|
||||
private final VaultConfig vaultConfig;
|
||||
private final Application application;
|
||||
private final Path exportDestination;
|
||||
|
||||
@Inject
|
||||
public ReportWriter(@HealthCheckWindow Vault vault, AtomicReference<VaultConfig> vaultConfigRef, Application application, Environment env) {
|
||||
this.vault = vault;
|
||||
this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
|
||||
this.application = application;
|
||||
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
|
||||
}
|
||||
|
||||
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
|
||||
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
|
||||
for (var task : tasks) {
|
||||
if (task.getState() == Worker.State.READY) {
|
||||
LOG.debug("Skipping not performed check {}.", task.getCheck().identifier());
|
||||
continue;
|
||||
}
|
||||
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier()));
|
||||
switch (task.getState()) {
|
||||
case SUCCEEDED -> {
|
||||
writer.write("STATUS: SUCCESS\nRESULTS:\n");
|
||||
for (var result : task.results()) {
|
||||
writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString()));
|
||||
}
|
||||
}
|
||||
case CANCELLED -> writer.write("STATUS: CANCELED\n");
|
||||
case FAILED -> {
|
||||
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier());
|
||||
writer.write(prepareFailureMsg(task));
|
||||
}
|
||||
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
|
||||
}
|
||||
}
|
||||
}
|
||||
reveal();
|
||||
}
|
||||
|
||||
private String prepareFailureMsg(HealthCheckTask task) {
|
||||
if (task.getException() != null) {
|
||||
return ExceptionUtils.getStackTrace(task.getException()) //
|
||||
.lines() //
|
||||
.map(line -> "\t\t" + line + "\n") //
|
||||
.collect(Collectors.joining());
|
||||
} else {
|
||||
return "Unknown reason of failure.";
|
||||
}
|
||||
}
|
||||
|
||||
private void reveal() {
|
||||
application.getHostServices().showDocument(exportDestination.getParent().toUri().toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.scene.control.Alert;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@HealthCheckScoped
|
||||
class ResultFixApplier {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
|
||||
|
||||
private final Path vaultPath;
|
||||
private final SecureRandom csprng;
|
||||
private final Masterkey masterkey;
|
||||
private final VaultConfig vaultConfig;
|
||||
|
||||
@Inject
|
||||
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
|
||||
this.vaultPath = vault.getPath();
|
||||
this.masterkey = masterkeyRef.get();
|
||||
this.vaultConfig = vaultConfigRef.get();
|
||||
this.csprng = csprng;
|
||||
}
|
||||
|
||||
public void fix(DiagnosticResult result) {
|
||||
Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result");
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to apply fix", e);
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
|
||||
alert.showAndWait();
|
||||
//TODO: real error/not supported handling
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
// unscoped because each cell needs its own controller
|
||||
public class ResultListCellController implements FxController {
|
||||
|
||||
private final ResultFixApplier fixApplier;
|
||||
private final ObjectProperty<DiagnosticResult> result;
|
||||
private final Binding<String> description;
|
||||
|
||||
public FontAwesome5IconView iconView;
|
||||
public Button actionButton;
|
||||
|
||||
@Inject
|
||||
public ResultListCellController(ResultFixApplier fixApplier) {
|
||||
this.result = new SimpleObjectProperty<>(null);
|
||||
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
|
||||
this.fixApplier = fixApplier;
|
||||
result.addListener(this::updateCellContent);
|
||||
}
|
||||
|
||||
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
|
||||
iconView.getStyleClass().clear();
|
||||
actionButton.setVisible(false);
|
||||
//TODO: see comment in case WARN
|
||||
actionButton.setManaged(false);
|
||||
switch (newVal.getServerity()) {
|
||||
case INFO -> {
|
||||
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
|
||||
iconView.getStyleClass().add("glyph-icon-muted");
|
||||
}
|
||||
case GOOD -> {
|
||||
iconView.setGlyph(FontAwesome5Icon.CHECK);
|
||||
iconView.getStyleClass().add("glyph-icon-primary");
|
||||
}
|
||||
case WARN -> {
|
||||
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
|
||||
iconView.getStyleClass().add("glyph-icon-orange");
|
||||
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
|
||||
// before both are not fix, do not show the button
|
||||
//actionButton.setVisible(true);
|
||||
}
|
||||
case CRITICAL -> {
|
||||
iconView.setGlyph(FontAwesome5Icon.TIMES);
|
||||
iconView.getStyleClass().add("glyph-icon-red");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void runResultAction() {
|
||||
final var realResult = result.get();
|
||||
if (realResult != null) {
|
||||
fixApplier.fix(realResult);
|
||||
}
|
||||
}
|
||||
/* Getter & Setter */
|
||||
|
||||
|
||||
public DiagnosticResult getResult() {
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public void setResult(DiagnosticResult result) {
|
||||
this.result.set(result);
|
||||
}
|
||||
|
||||
public ObjectProperty<DiagnosticResult> resultProperty() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description.getValue();
|
||||
}
|
||||
|
||||
public Binding<String> descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.util.Callback;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
|
||||
|
||||
private final FxmlLoaderFactory fxmlLoaders;
|
||||
|
||||
@Inject
|
||||
ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
this.fxmlLoaders = fxmlLoaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
|
||||
try {
|
||||
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
|
||||
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to load /fxml/health_result_listcell.fxml.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cell extends ListCell<DiagnosticResult> {
|
||||
|
||||
private final Parent node;
|
||||
private final ResultListCellController controller;
|
||||
|
||||
public Cell(Parent node, ResultListCellController controller) {
|
||||
this.node = node;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(DiagnosticResult item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item == null || empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
controller.setResult(item);
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||
import org.cryptomator.cryptofs.VaultKeyInvalidException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class StartController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||
private final ExecutorService executor;
|
||||
private final AtomicReference<Masterkey> masterkeyRef;
|
||||
private final AtomicReference<VaultConfig> vaultConfigRef;
|
||||
private final Lazy<Scene> checkScene;
|
||||
private final Lazy<ErrorComponent.Builder> errorComponent;
|
||||
|
||||
/* FXML */
|
||||
|
||||
@Inject
|
||||
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
|
||||
this.window = window;
|
||||
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||
this.executor = executor;
|
||||
this.masterkeyRef = masterkeyRef;
|
||||
this.vaultConfigRef = vaultConfigRef;
|
||||
this.checkScene = checkScene;
|
||||
this.errorComponent = errorComponent;
|
||||
|
||||
//TODO: this is ugly
|
||||
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
|
||||
//or: load vault config in a previous step to see if it is loadable.
|
||||
VaultConfig.UnverifiedVaultConfig tmp;
|
||||
try {
|
||||
tmp = vault.getUnverifiedVaultConfig();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
tmp = null;
|
||||
}
|
||||
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
LOG.trace("StartController.close()");
|
||||
window.close();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
LOG.trace("StartController.next()");
|
||||
executor.submit(this::loadKey);
|
||||
}
|
||||
|
||||
private void loadKey() {
|
||||
assert !Platform.isFxApplicationThread();
|
||||
assert unverifiedVaultConfig.isPresent();
|
||||
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
|
||||
var unverifiedCfg = unverifiedVaultConfig.get();
|
||||
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
|
||||
vaultConfigRef.set(verifiedCfg);
|
||||
var old = masterkeyRef.getAndSet(masterkey.clone());
|
||||
if (old != null) {
|
||||
old.destroy();
|
||||
}
|
||||
Platform.runLater(this::loadedKey);
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||
// retry
|
||||
loadKey();
|
||||
} else {
|
||||
Platform.runLater(() -> loadingKeyFailed(e));
|
||||
}
|
||||
} catch (VaultKeyInvalidException e) {
|
||||
Platform.runLater(() -> loadingKeyFailed(e));
|
||||
} catch (VaultConfigLoadException e) {
|
||||
Platform.runLater(() -> loadingKeyFailed(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void loadedKey() {
|
||||
LOG.debug("Loaded valid key");
|
||||
window.setScene(checkScene.get());
|
||||
}
|
||||
|
||||
private void loadingKeyFailed(Exception e) {
|
||||
if (e instanceof UnlockCancelledException) {
|
||||
// ok
|
||||
} else if (e instanceof VaultKeyInvalidException) {
|
||||
LOG.error("Invalid key"); //TODO: specific error screen
|
||||
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||
} else {
|
||||
LOG.error("Failed to load key.", e);
|
||||
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isInvalidConfig() {
|
||||
return unverifiedVaultConfig.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface KeyLoading {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@KeyLoadingScoped
|
||||
@Subcomponent(modules = {KeyLoadingModule.class})
|
||||
public interface KeyLoadingComponent {
|
||||
|
||||
@KeyLoading
|
||||
KeyLoadingStrategy keyloadingStrategy();
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@KeyLoading Vault vault);
|
||||
|
||||
@BindsInstance
|
||||
Builder window(@KeyLoading Stage window);
|
||||
|
||||
KeyLoadingComponent build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module(includes = {MasterkeyFileLoadingModule.class})
|
||||
abstract class KeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||
try {
|
||||
String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme();
|
||||
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||
} catch (IOException e) {
|
||||
return KeyLoadingStrategy.failed(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import javax.inject.Scope;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface KeyLoadingScoped {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface KeyLoadingStrategy extends MasterkeyLoader {
|
||||
|
||||
/**
|
||||
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
|
||||
* <p>
|
||||
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
|
||||
*
|
||||
* @param keyId An URI uniquely identifying the source and identity of the key
|
||||
* @return The raw key bytes. Must not be null
|
||||
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
|
||||
*/
|
||||
@Override
|
||||
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
|
||||
|
||||
/**
|
||||
* Allows the loader to try and recover from an exception thrown during the last attempt.
|
||||
*
|
||||
* @param exception An exception thrown by {@link #loadKey(URI)}.
|
||||
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
|
||||
*/
|
||||
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release any ressources or do follow-up tasks after loading a key.
|
||||
*
|
||||
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
|
||||
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
|
||||
*/
|
||||
default void cleanup(boolean unlockedSuccessfully) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}.
|
||||
*
|
||||
* @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped.
|
||||
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
|
||||
*/
|
||||
static KeyLoadingStrategy failed(Exception exception) {
|
||||
return keyid -> {
|
||||
if (exception instanceof MasterkeyLoadingFailedException e) {
|
||||
throw e;
|
||||
} else {
|
||||
throw new MasterkeyLoadingFailedException("Can not load key", exception);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
class MasterkeyFileLoadingFinisher {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
|
||||
|
||||
private final Vault vault;
|
||||
private final Optional<char[]> storedPassword;
|
||||
private final AtomicReference<char[]> enteredPassword;
|
||||
private final AtomicBoolean shouldSavePassword;
|
||||
private final KeychainManager keychain;
|
||||
|
||||
@Inject
|
||||
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
|
||||
this.vault = vault;
|
||||
this.storedPassword = storedPassword;
|
||||
this.enteredPassword = enteredPassword;
|
||||
this.shouldSavePassword = shouldSavePassword;
|
||||
this.keychain = keychain;
|
||||
}
|
||||
|
||||
public void cleanup(boolean successfullyUnlocked) {
|
||||
if (successfullyUnlocked && shouldSavePassword.get()) {
|
||||
savePasswordToSystemkeychain();
|
||||
}
|
||||
wipePassword(storedPassword.orElse(null));
|
||||
wipePassword(enteredPassword.getAndSet(null));
|
||||
}
|
||||
|
||||
private void savePasswordToSystemkeychain() {
|
||||
if (keychain.isSupported()) {
|
||||
try {
|
||||
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void wipePassword(char[] pw) {
|
||||
if (pw != null) {
|
||||
Arrays.fill(pw, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import dagger.multibindings.StringKey;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module(subcomponents = {ForgetPasswordComponent.class})
|
||||
public abstract class MasterkeyFileLoadingModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
|
||||
|
||||
public enum PasswordEntry {
|
||||
PASSWORD_ENTERED,
|
||||
CANCELED
|
||||
}
|
||||
|
||||
public enum MasterkeyFileProvision {
|
||||
MASTERKEYFILE_PROVIDED,
|
||||
CANCELED
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
|
||||
return new UserInteractionLock<>(null);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
|
||||
return new UserInteractionLock<>(null);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savedPassword")
|
||||
@KeyLoadingScoped
|
||||
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
|
||||
if (!keychain.isSupported() || keychain.isLocked()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
try {
|
||||
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to load entry from system keychain.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicReference<>(storedPassword.orElse(null));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savePassword")
|
||||
@KeyLoadingScoped
|
||||
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicBoolean(storedPassword.isPresent());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE)
|
||||
@KeyLoadingScoped
|
||||
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(PassphraseEntryController.class)
|
||||
abstract FxController bindUnlockController(PassphraseEntryController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(SelectMasterkeyFileController.class)
|
||||
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@KeyLoadingScoped
|
||||
@StringKey(MasterkeyFileLoadingStrategy.SCHEME)
|
||||
abstract KeyLoadingStrategy bindMasterkeyFileLoadingStrategy(MasterkeyFileLoadingStrategy strategy);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.net.URI;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoading
|
||||
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
|
||||
public static final String SCHEME = "masterkeyfile";
|
||||
|
||||
private final Vault vault;
|
||||
private final MasterkeyFileAccess masterkeyFileAcccess;
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> passphraseEntryScene;
|
||||
private final Lazy<Scene> selectMasterkeyFileScene;
|
||||
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
|
||||
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicReference<Path> filePath;
|
||||
private final MasterkeyFileLoadingFinisher finisher;
|
||||
|
||||
private boolean wrongPassword;
|
||||
|
||||
@Inject
|
||||
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
|
||||
this.vault = vault;
|
||||
this.masterkeyFileAcccess = masterkeyFileAcccess;
|
||||
this.window = window;
|
||||
this.passphraseEntryScene = passphraseEntryScene;
|
||||
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
|
||||
this.passwordEntryLock = passwordEntryLock;
|
||||
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||
this.password = password;
|
||||
this.filePath = filePath;
|
||||
this.finisher = finisher;
|
||||
}
|
||||
|
||||
@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 masterkeyFileAcccess.load(filePath, passphrase);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new UnlockCancelledException("Unlock interrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||
if (exception instanceof InvalidPassphraseException) {
|
||||
this.wrongPassword = true;
|
||||
password.set(null);
|
||||
return true; // reattempting key load
|
||||
} else {
|
||||
return false; // nothing we can do
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(boolean unlockedSuccessfully) {
|
||||
finisher.cleanup(unlockedSuccessfully);
|
||||
}
|
||||
|
||||
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
|
||||
if (filePath.get() == null) {
|
||||
return switch (askUserForMasterkeyFilePath()) {
|
||||
case MASTERKEYFILE_PROVIDED -> filePath.get();
|
||||
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
|
||||
};
|
||||
} else {
|
||||
return filePath.get();
|
||||
}
|
||||
}
|
||||
|
||||
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(selectMasterkeyFileScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
});
|
||||
return masterkeyFileProvisionLock.awaitInteraction();
|
||||
}
|
||||
|
||||
private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
|
||||
if (password.get() == null) {
|
||||
return switch (askForPassphrase()) {
|
||||
case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
|
||||
case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
|
||||
};
|
||||
} else {
|
||||
// e.g. pre-filled from keychain or previous unlock attempt
|
||||
return CharBuffer.wrap(password.get());
|
||||
}
|
||||
}
|
||||
|
||||
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(passphraseEntryScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
if (wrongPassword) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
}
|
||||
});
|
||||
return passwordEntryLock.awaitInteraction();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
@@ -7,6 +7,9 @@ import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.common.WeakBindings;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -38,17 +41,17 @@ import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@UnlockScoped
|
||||
public class UnlockController implements FxController {
|
||||
@KeyLoadingScoped
|
||||
public class PassphraseEntryController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicBoolean savePassword;
|
||||
private final Optional<char[]> savedPassword;
|
||||
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
|
||||
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
|
||||
private final ForgetPasswordComponent.Builder forgetPassword;
|
||||
private final KeychainManager keychain;
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
|
||||
@@ -66,7 +69,7 @@ public class UnlockController implements FxController {
|
||||
public Animation unlockAnimation;
|
||||
|
||||
@Inject
|
||||
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.password = password;
|
||||
@@ -138,7 +141,7 @@ public class UnlockController implements FxController {
|
||||
// if not already interacted, mark this workflow as cancelled:
|
||||
if (passwordEntryLock.awaitingInteraction().get()) {
|
||||
LOG.debug("Unlock canceled by user.");
|
||||
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
|
||||
passwordEntryLock.interacted(PasswordEntry.CANCELED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ public class UnlockController implements FxController {
|
||||
if (oldPw != null) {
|
||||
Arrays.fill(oldPw, ' ');
|
||||
}
|
||||
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
|
||||
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
|
||||
startUnlockAnimation();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@KeyLoadingScoped
|
||||
public class SelectMasterkeyFileController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final AtomicReference<Path> masterkeyPath;
|
||||
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
@Inject
|
||||
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.masterkeyPath = masterkeyPath;
|
||||
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.window.setOnHiding(this::windowClosed);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void cancel() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
private void windowClosed(WindowEvent windowEvent) {
|
||||
// if not already interacted, mark this workflow as cancelled:
|
||||
if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
|
||||
LOG.debug("Unlock canceled by user.");
|
||||
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void proceed() {
|
||||
LOG.trace("proceed()");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
LOG.debug("Chose masterkey file: {}", masterkeyFile);
|
||||
masterkeyPath.set(masterkeyFile.toPath());
|
||||
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import javafx.application.Platform;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
@@ -78,7 +79,7 @@ class AppLaunchEventHandler {
|
||||
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
|
||||
}
|
||||
LOG.debug("Added vault {}", potentialVaultPath);
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to add vault " + potentialVaultPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.cryptomator.ui.launcher;
|
||||
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
@@ -24,11 +25,13 @@ import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
|
||||
@Singleton
|
||||
public class AppLifecycleListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
|
||||
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
|
||||
public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
|
||||
|
||||
private final FxApplicationStarter fxApplicationStarter;
|
||||
private final CountDownLatch shutdownLatch;
|
||||
@@ -127,6 +130,8 @@ public class AppLifecycleListener {
|
||||
vault.lock(true);
|
||||
} catch (Volume.VolumeException e) {
|
||||
LOG.error("Failed to unmount vault " + vault.getPath(), e);
|
||||
} catch (LockNotCompletedException e) {
|
||||
LOG.error("Failed to lock vault " + vault.getPath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.cryptomator.ui.lock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
@@ -35,21 +37,23 @@ public class LockWorkflow extends Task<Void> {
|
||||
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
|
||||
private final Lazy<Scene> lockForcedScene;
|
||||
private final Lazy<Scene> lockFailedScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
|
||||
@Inject
|
||||
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
|
||||
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
|
||||
this.lockWindow = lockWindow;
|
||||
this.vault = vault;
|
||||
this.forceLockDecisionLock = forceLockDecisionLock;
|
||||
this.lockForcedScene = lockForcedScene;
|
||||
this.lockFailedScene = lockFailedScene;
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException {
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
|
||||
try {
|
||||
vault.lock(false);
|
||||
} catch (Volume.VolumeException e) {
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
|
||||
var decision = askUserForAction();
|
||||
switch (decision) {
|
||||
@@ -77,29 +81,29 @@ public class LockWorkflow extends Task<Void> {
|
||||
return forceLockDecisionLock.awaitInteraction();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
LOG.info("Lock of {} succeeded.", vault.getDisplayName());
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
LOG.warn("Failed to lock {}.", vault.getDisplayName());
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
lockWindow.setScene(lockFailedScene.get());
|
||||
lockWindow.show();
|
||||
final var throwable = super.getException();
|
||||
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
if (throwable instanceof Volume.VolumeException) {
|
||||
lockWindow.setScene(lockFailedScene.get());
|
||||
lockWindow.show();
|
||||
} else {
|
||||
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
LOG.debug("Lock of {} canceled.", vault.getDisplayName());
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.cryptomator.ui.mainwindow;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.DirStructure;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -20,6 +22,7 @@ import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
@@ -27,6 +30,7 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
|
||||
@MainWindowScoped
|
||||
public class MainWindowController implements FxController {
|
||||
@@ -91,23 +95,21 @@ public class MainWindowController implements FxController {
|
||||
}
|
||||
|
||||
private boolean containsVault(Path path) {
|
||||
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
||||
return true;
|
||||
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addVault(Path pathToVault) {
|
||||
try {
|
||||
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
||||
if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
|
||||
vaultListManager.add(pathToVault.getParent());
|
||||
} else {
|
||||
vaultListManager.add(pathToVault);
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.debug("Not a vault: {}", pathToVault);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
import org.cryptomator.ui.migration.MigrationComponent;
|
||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
@@ -27,7 +28,7 @@ import javafx.stage.StageStyle;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
|
||||
@Module(subcomponents = {AddVaultWizardComponent.class, HealthCheckComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
|
||||
abstract class MainWindowModule {
|
||||
|
||||
@Provides
|
||||
@@ -86,6 +87,11 @@ abstract class MainWindowModule {
|
||||
@FxControllerKey(VaultListController.class)
|
||||
abstract FxController bindVaultListController(VaultListController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(VaultListContextMenuController.class)
|
||||
abstract FxController bindVaultListContextMenuController(VaultListContextMenuController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(VaultDetailController.class)
|
||||
|
||||
@@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void showDonationKeyPreferences() {
|
||||
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
|
||||
application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
@@ -32,7 +32,8 @@ public class VaultDetailController implements FxController {
|
||||
this.anyVaultSelected = vault.isNotNull();
|
||||
}
|
||||
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
|
||||
// TODO deduplicate w/ VaultListCellController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
return switch (state) {
|
||||
case LOCKED -> FontAwesome5Icon.LOCK;
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
|
||||
@@ -13,6 +14,7 @@ import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Optional;
|
||||
@@ -28,13 +30,13 @@ public class VaultDetailLockedController implements FxController {
|
||||
private final BooleanExpression passwordSaved;
|
||||
|
||||
@Inject
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
||||
this.vault = vault;
|
||||
this.application = application;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
this.keychain = keychain;
|
||||
this.mainWindow = mainWindow;
|
||||
if (keychain.isSupported()) {
|
||||
if (keychain.isSupported() && !keychain.isLocked()) {
|
||||
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychain.getPassphraseStoredProperty(v.getId())));
|
||||
} else {
|
||||
this.passwordSaved = new SimpleBooleanProperty(false);
|
||||
|
||||
@@ -24,7 +24,8 @@ public class VaultListCellController implements FxController {
|
||||
.map(this::getGlyphForVaultState);
|
||||
}
|
||||
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
|
||||
// TODO deduplicate w/ VaultDetailController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
return switch (state) {
|
||||
case LOCKED -> FontAwesome5Icon.LOCK;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
|
||||
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.fxapp.FxApplication;
|
||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultListContextMenuController implements FxController {
|
||||
|
||||
private final ObservableOptionalValue<Vault> selectedVault;
|
||||
private final Stage mainWindow;
|
||||
private final FxApplication application;
|
||||
private final KeychainManager keychain;
|
||||
private final RemoveVaultComponent.Builder removeVault;
|
||||
private final VaultOptionsComponent.Builder vaultOptionsWindow;
|
||||
private final OptionalBinding<VaultState.Value> selectedVaultState;
|
||||
private final Binding<Boolean> selectedVaultPassphraseStored;
|
||||
private final Binding<Boolean> selectedVaultRemovable;
|
||||
private final Binding<Boolean> selectedVaultUnlockable;
|
||||
private final Binding<Boolean> selectedVaultLockable;
|
||||
|
||||
@Inject
|
||||
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplication application, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
|
||||
this.selectedVault = EasyBind.wrapNullable(selectedVault);
|
||||
this.mainWindow = mainWindow;
|
||||
this.application = application;
|
||||
this.keychain = keychain;
|
||||
this.removeVault = removeVault;
|
||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||
|
||||
this.selectedVaultState = this.selectedVault.mapObservable(Vault::stateProperty);
|
||||
this.selectedVaultPassphraseStored = this.selectedVault.map(this::isPasswordStored).orElse(false);
|
||||
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
|
||||
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
|
||||
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
|
||||
}
|
||||
|
||||
private boolean isPasswordStored(Vault vault) {
|
||||
return keychain.getPassphraseStoredProperty(vault.getId()).get();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickRemoveVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
removeVault.vault(v).build().showRemoveVault();
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickShowVaultOptions() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
vaultOptionsWindow.vault(v).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickUnlockVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
application.startUnlockWorkflow(v, Optional.of(mainWindow));
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickLockVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
application.startLockWorkflow(v, Optional.of(mainWindow));
|
||||
});
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickRevealVault() {
|
||||
selectedVault.ifValuePresent(v -> {
|
||||
application.getVaultService().reveal(v);
|
||||
});
|
||||
}
|
||||
|
||||
// Getter and Setter
|
||||
|
||||
public Binding<Boolean> selectedVaultUnlockableProperty() {
|
||||
return selectedVaultUnlockable;
|
||||
}
|
||||
|
||||
public boolean isSelectedVaultUnlockable() {
|
||||
return selectedVaultUnlockable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultLockableProperty() {
|
||||
return selectedVaultLockable;
|
||||
}
|
||||
|
||||
public boolean isSelectedVaultLockable() {
|
||||
return selectedVaultLockable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultRemovableProperty() {
|
||||
return selectedVaultRemovable;
|
||||
}
|
||||
|
||||
public boolean isSelectedVaultRemovable() {
|
||||
return selectedVaultRemovable.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> selectedVaultPassphraseStoredProperty() {
|
||||
return selectedVaultPassphraseStored;
|
||||
}
|
||||
|
||||
public boolean isSelectedVaultPassphraseStored() {
|
||||
return selectedVaultPassphraseStored.getValue();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -17,30 +16,43 @@ import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultListController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
|
||||
|
||||
private final Stage mainWindow;
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final ObjectProperty<Vault> selectedVault;
|
||||
private final VaultListCellFactory cellFactory;
|
||||
private final AddVaultWizardComponent.Builder addVaultWizard;
|
||||
private final RemoveVaultComponent.Builder removeVault;
|
||||
private final BooleanBinding noVaultSelected;
|
||||
private final BooleanBinding emptyVaultList;
|
||||
private final RemoveVaultComponent.Builder removeVaultDialogue;
|
||||
|
||||
public ListView<Vault> vaultList;
|
||||
|
||||
@Inject
|
||||
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
|
||||
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.vaults = vaults;
|
||||
this.selectedVault = selectedVault;
|
||||
this.cellFactory = cellFactory;
|
||||
this.addVaultWizard = addVaultWizard;
|
||||
this.removeVault = removeVault;
|
||||
this.noVaultSelected = selectedVault.isNull();
|
||||
this.removeVaultDialogue = removeVaultDialogue;
|
||||
|
||||
this.emptyVaultList = Bindings.isEmpty(vaults);
|
||||
|
||||
selectedVault.addListener(this::selectedVaultDidChange);
|
||||
}
|
||||
|
||||
@@ -56,6 +68,41 @@ public class VaultListController implements FxController {
|
||||
}
|
||||
}
|
||||
});
|
||||
vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect);
|
||||
vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> {
|
||||
if (selectedVault.get() == null) {
|
||||
request.consume();
|
||||
}
|
||||
});
|
||||
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
if (keyEvent.getCode() == KeyCode.DELETE) {
|
||||
pressedShortcutToRemoveVault();
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
if (keyEvent.getCode() == KeyCode.BACK_SPACE) {
|
||||
pressedShortcutToRemoveVault();
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//register vault selection shortcut to the main window
|
||||
mainWindow.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
|
||||
if (keyEvent.isShortcutDown() && keyEvent.getCode().isDigitKey()) {
|
||||
vaultList.getSelectionModel().select(Integer.parseInt(keyEvent.getText()) - 1);
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deselect(MouseEvent released) {
|
||||
if (released.getY() > (vaultList.getItems().size() * vaultList.fixedCellSizeProperty().get())) {
|
||||
vaultList.getSelectionModel().clearSelection();
|
||||
released.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
|
||||
@@ -70,13 +117,10 @@ public class VaultListController implements FxController {
|
||||
addVaultWizard.build().showAddVaultWizard();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickRemoveVault() {
|
||||
Vault v = selectedVault.get();
|
||||
if (v != null) {
|
||||
removeVault.vault(v).build().showRemoveVault();
|
||||
} else {
|
||||
LOG.debug("Cannot remove a vault if none is selected.");
|
||||
private void pressedShortcutToRemoveVault() {
|
||||
final var vault = selectedVault.get();
|
||||
if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) {
|
||||
removeVaultDialogue.vault(vault).build().showRemoveVault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,11 +134,4 @@ public class VaultListController implements FxController {
|
||||
return emptyVaultList.get();
|
||||
}
|
||||
|
||||
public BooleanBinding noVaultSelectedProperty() {
|
||||
return noVaultSelected;
|
||||
}
|
||||
|
||||
public boolean isNoVaultSelected() {
|
||||
return noVaultSelected.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -43,6 +44,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
|
||||
@MigrationScoped
|
||||
public class MigrationRunController implements FxController {
|
||||
@@ -89,7 +91,12 @@ public class MigrationRunController implements FxController {
|
||||
if (keychain.isSupported()) {
|
||||
loadStoredPassword();
|
||||
}
|
||||
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
|
||||
|
||||
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
|
||||
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
|
||||
.or(passwordField.textProperty().isEmpty()));
|
||||
|
||||
window.setOnHiding(event -> passwordField.wipe());
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -101,7 +108,7 @@ public class MigrationRunController implements FxController {
|
||||
public void migrate() {
|
||||
LOG.info("Migrating vault {}", vault.getPath());
|
||||
CharSequence password = passwordField.getCharacters();
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
vault.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING);
|
||||
passwordField.setDisable(true);
|
||||
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
|
||||
Platform.runLater(() -> {
|
||||
@@ -110,15 +117,15 @@ public class MigrationRunController implements FxController {
|
||||
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
|
||||
Tasks.create(() -> {
|
||||
Migrators migrators = Migrators.get();
|
||||
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
||||
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
|
||||
migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
||||
return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME);
|
||||
}).onSuccess(needsAnotherMigration -> {
|
||||
if (needsAnotherMigration) {
|
||||
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
} else {
|
||||
LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
passwordField.wipe();
|
||||
window.setScene(successScene.get());
|
||||
}
|
||||
@@ -127,20 +134,20 @@ public class MigrationRunController implements FxController {
|
||||
passwordField.setDisable(false);
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
|
||||
LOG.error("Underlying file system not supported.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
missingCapability.set(e.getMissingCapability());
|
||||
window.setScene(capabilityErrorScene.get());
|
||||
}).onError(FileNameTooLongException.class, e -> {
|
||||
LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
window.setScene(impossibleScene.get());
|
||||
}).onError(Exception.class, e -> { // including RuntimeExceptions
|
||||
LOG.error("Migration failed for technical reasons.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
passwordField.setDisable(false);
|
||||
|
||||
@@ -157,8 +157,8 @@ public class GeneralPreferencesController implements FxController {
|
||||
|
||||
|
||||
@FXML
|
||||
public void showDonationTab() {
|
||||
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
|
||||
public void showContributeTab() {
|
||||
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -26,7 +26,7 @@ public class PreferencesController implements FxController {
|
||||
public Tab generalTab;
|
||||
public Tab volumeTab;
|
||||
public Tab updatesTab;
|
||||
public Tab donationKeyTab;
|
||||
public Tab contributeTab;
|
||||
public Tab aboutTab;
|
||||
|
||||
@Inject
|
||||
@@ -52,7 +52,7 @@ public class PreferencesController implements FxController {
|
||||
return switch (selectedTab) {
|
||||
case UPDATES -> updatesTab;
|
||||
case VOLUME -> volumeTab;
|
||||
case DONATION_KEY -> donationKeyTab;
|
||||
case CONTRIBUTE -> contributeTab;
|
||||
case GENERAL -> generalTab;
|
||||
case ABOUT -> aboutTab;
|
||||
case ANY -> updateAvailable.get() ? updatesTab : generalTab;
|
||||
|
||||
@@ -77,8 +77,8 @@ abstract class PreferencesModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(DonationKeyPreferencesController.class)
|
||||
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
|
||||
@FxControllerKey(SupporterCertificateController.class)
|
||||
abstract FxController bindSupporterCertificatePreferencesController(SupporterCertificateController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
||||
@@ -22,9 +22,9 @@ public enum SelectedPreferencesTab {
|
||||
UPDATES,
|
||||
|
||||
/**
|
||||
* Show donation key tab
|
||||
* Show contribute tab
|
||||
*/
|
||||
DONATION_KEY,
|
||||
CONTRIBUTE,
|
||||
|
||||
/**
|
||||
* Show about tab
|
||||
|
||||
@@ -14,17 +14,17 @@ import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
|
||||
@PreferencesScoped
|
||||
public class DonationKeyPreferencesController implements FxController {
|
||||
public class SupporterCertificateController implements FxController {
|
||||
|
||||
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
|
||||
private static final String SUPPORTER_URI = "https://store.cryptomator.org/desktop";
|
||||
|
||||
private final Application application;
|
||||
private final LicenseHolder licenseHolder;
|
||||
private final Settings settings;
|
||||
public TextArea donationKeyField;
|
||||
public TextArea supporterCertificateField;
|
||||
|
||||
@Inject
|
||||
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
|
||||
SupporterCertificateController(Application application, LicenseHolder licenseHolder, Settings settings) {
|
||||
this.application = application;
|
||||
this.licenseHolder = licenseHolder;
|
||||
this.settings = settings;
|
||||
@@ -32,9 +32,9 @@ public class DonationKeyPreferencesController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
|
||||
donationKeyField.textProperty().addListener(this::registrationKeyChanged);
|
||||
donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null));
|
||||
supporterCertificateField.textProperty().addListener(this::registrationKeyChanged);
|
||||
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
}
|
||||
|
||||
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
|
||||
@@ -53,8 +53,8 @@ public class DonationKeyPreferencesController implements FxController {
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void getDonationKey() {
|
||||
application.getHostServices().showDocument(DONATION_URI);
|
||||
public void getSupporterCertificate() {
|
||||
application.getHostServices().showDocument(SUPPORTER_URI);
|
||||
}
|
||||
|
||||
public LicenseHolder getLicenseHolder() {
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException {
|
||||
protected String call() throws IOException, CryptoException {
|
||||
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,34 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.hash.Hashing;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@Singleton
|
||||
public class RecoveryKeyFactory {
|
||||
|
||||
private static final byte[] PEPPER = new byte[0];
|
||||
|
||||
private final WordEncoder wordEncoder;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyFactory(WordEncoder wordEncoder) {
|
||||
public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.wordEncoder = wordEncoder;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
}
|
||||
|
||||
public Collection<String> getDictionary() {
|
||||
@@ -36,11 +42,14 @@ public class RecoveryKeyFactory {
|
||||
* @return The recovery key of the vault at the given path
|
||||
* @throws IOException If the masterkey file could not be read
|
||||
* @throws InvalidPassphraseException If the provided password is wrong
|
||||
* @throws CryptoException In case of other cryptographic errors
|
||||
* @apiNote This is a long-running operation and should be invoked in a background thread
|
||||
*/
|
||||
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
|
||||
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
|
||||
try {
|
||||
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
|
||||
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||
byte[] rawKey = new byte[0];
|
||||
try (var masterkey = masterkeyFileAccess.load(masterkeyPath, password)) {
|
||||
rawKey = masterkey.getEncoded();
|
||||
return createRecoveryKey(rawKey);
|
||||
} finally {
|
||||
Arrays.fill(rawKey, (byte) 0x00);
|
||||
@@ -72,8 +81,15 @@ public class RecoveryKeyFactory {
|
||||
*/
|
||||
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
|
||||
final byte[] rawKey = decodeRecoveryKey(recoveryKey);
|
||||
try {
|
||||
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
|
||||
try (var masterkey = new Masterkey(rawKey)) {
|
||||
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||
if (Files.exists(masterkeyPath)) {
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
// TODO: deduplicate with ChangePasswordController:
|
||||
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
|
||||
} finally {
|
||||
Arrays.fill(rawKey, (byte) 0x00);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||
@@ -17,8 +17,6 @@ import org.cryptomator.ui.common.StageFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.Scene;
|
||||
@@ -56,14 +54,6 @@ abstract class RecoveryKeyModule {
|
||||
return new SimpleStringProperty();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@RecoveryKeyScoped
|
||||
@Named("newPassword")
|
||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
||||
return new SimpleObjectProperty<>("");
|
||||
}
|
||||
|
||||
|
||||
// ------------------
|
||||
|
||||
@Provides
|
||||
@@ -126,8 +116,8 @@ abstract class RecoveryKeyModule {
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(NewPasswordController.class)
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||
return new NewPasswordController(resourceBundle, strengthRater);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,14 +6,12 @@ import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.NewPasswordController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
@@ -32,21 +30,19 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||
private final ExecutorService executor;
|
||||
private final StringProperty recoveryKey;
|
||||
private final ObjectProperty<CharSequence> newPassword;
|
||||
private final Lazy<Scene> recoverScene;
|
||||
private final BooleanBinding invalidNewPassword;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
|
||||
public NewPasswordController newPasswordController;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword") ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
|
||||
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
this.executor = executor;
|
||||
this.recoveryKey = recoveryKey;
|
||||
this.newPassword = newPassword;
|
||||
this.recoverScene = recoverScene;
|
||||
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@@ -81,7 +77,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
|
||||
@Override
|
||||
protected Void call() throws IOException, IllegalArgumentException {
|
||||
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get());
|
||||
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -89,11 +85,12 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public BooleanBinding invalidNewPasswordProperty() {
|
||||
return invalidNewPassword;
|
||||
public ReadOnlyBooleanProperty validPasswordProperty() {
|
||||
return newPasswordController.passwordsMatchAndSufficientProperty();
|
||||
}
|
||||
|
||||
public boolean isInvalidNewPassword() {
|
||||
return newPassword.get() == null || newPassword.get().length() == 0;
|
||||
public boolean isValidPassword() {
|
||||
return newPasswordController.passwordsMatchAndSufficientProperty().get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
|
||||
var weakStage = new WeakReference<>(stage);
|
||||
vault.stateProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
|
||||
if (newValue != VaultState.UNLOCKED) {
|
||||
public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) {
|
||||
if (newValue != VaultState.Value.UNLOCKED) {
|
||||
Stage stage = weakStage.get();
|
||||
if (stage != null) {
|
||||
stage.hide();
|
||||
|
||||
@@ -44,6 +44,9 @@ class TrayMenuController {
|
||||
|
||||
public void initTrayMenu() {
|
||||
vaults.addListener(this::vaultListChanged);
|
||||
vaults.forEach(v -> {
|
||||
v.displayNameProperty().addListener(this::vaultListChanged);
|
||||
});
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
|
||||
|
||||
public UnlockCancelledException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnlockCancelledException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
|
||||
@@ -17,19 +13,17 @@ import javafx.stage.Stage;
|
||||
public class UnlockInvalidMountPointController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> unlockScene;
|
||||
private final Vault vault;
|
||||
|
||||
@Inject
|
||||
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
|
||||
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
|
||||
this.window = window;
|
||||
this.unlockScene = unlockScene;
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
window.setScene(unlockScene.get());
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
@@ -4,20 +4,16 @@ import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@@ -27,54 +23,10 @@ import javafx.stage.Stage;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module(subcomponents = {ForgetPasswordComponent.class})
|
||||
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||
abstract class UnlockModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
|
||||
|
||||
public enum PasswordEntry {
|
||||
PASSWORD_ENTERED,
|
||||
CANCELED
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
|
||||
return new UserInteractionLock<>(null);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savedPassword")
|
||||
@UnlockScoped
|
||||
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow Vault vault) {
|
||||
if (!keychain.isSupported()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
try {
|
||||
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to load entry from system keychain.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicReference(storedPassword.orElse(null));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savePassword")
|
||||
@UnlockScoped
|
||||
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicBoolean(storedPassword.isPresent());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockWindow
|
||||
@UnlockScoped
|
||||
@@ -99,10 +51,10 @@ abstract class UnlockModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.UNLOCK)
|
||||
@UnlockWindow
|
||||
@UnlockScoped
|
||||
static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.UNLOCK);
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -121,11 +73,6 @@ abstract class UnlockModule {
|
||||
|
||||
// ------------------
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnlockController.class)
|
||||
abstract FxController bindUnlockController(UnlockController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnlockSuccessController.class)
|
||||
|
||||
@@ -1,40 +1,31 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A multi-step task that consists of background activities as well as user interaction.
|
||||
@@ -49,113 +40,47 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final VaultService vaultService;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicBoolean savePassword;
|
||||
private final Optional<char[]> savedPassword;
|
||||
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
|
||||
private final KeychainManager keychain;
|
||||
private final Lazy<Scene> unlockScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final Lazy<Scene> invalidMountPointScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||
|
||||
@Inject
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vaultService = vaultService;
|
||||
this.password = password;
|
||||
this.savePassword = savePassword;
|
||||
this.savedPassword = savedPassword;
|
||||
this.passwordEntryLock = passwordEntryLock;
|
||||
this.keychain = keychain;
|
||||
this.unlockScene = unlockScene;
|
||||
this.successScene = successScene;
|
||||
this.invalidMountPointScene = invalidMountPointScene;
|
||||
this.errorComponent = errorComponent;
|
||||
|
||||
setOnFailed(event -> {
|
||||
Throwable throwable = event.getSource().getException();
|
||||
if (throwable instanceof InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
} else {
|
||||
handleGenericError(throwable);
|
||||
}
|
||||
});
|
||||
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
try {
|
||||
if (attemptUnlock()) {
|
||||
handleSuccess();
|
||||
return true;
|
||||
attemptUnlock();
|
||||
return true;
|
||||
} catch (UnlockCancelledException e) {
|
||||
cancel(false); // set Tasks state to cancelled
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
boolean success = false;
|
||||
try {
|
||||
vault.unlock(keyLoadingStrategy);
|
||||
success = true;
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||
attemptUnlock();
|
||||
} else {
|
||||
cancel(false); // set Tasks state to cancelled
|
||||
return false;
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
wipePassword(password.get());
|
||||
wipePassword(savedPassword.orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
|
||||
while (proceed) {
|
||||
try {
|
||||
vault.unlock(CharBuffer.wrap(password.get()));
|
||||
return true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(unlockScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
if (animateShake) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
}
|
||||
});
|
||||
return passwordEntryLock.awaitInteraction();
|
||||
}
|
||||
|
||||
private void handleSuccess() {
|
||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
||||
if (savePassword.get()) {
|
||||
savePasswordToSystemkeychain();
|
||||
}
|
||||
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
|
||||
case ASK -> Platform.runLater(() -> {
|
||||
window.setScene(successScene.get());
|
||||
window.show();
|
||||
});
|
||||
case REVEAL -> {
|
||||
Platform.runLater(window::close);
|
||||
vaultService.reveal(vault);
|
||||
}
|
||||
case IGNORE -> Platform.runLater(window::close);
|
||||
}
|
||||
}
|
||||
|
||||
private void savePasswordToSystemkeychain() {
|
||||
if (keychain.isSupported()) {
|
||||
try {
|
||||
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||
}
|
||||
keyLoadingStrategy.cleanup(success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,15 +98,12 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
|
||||
}
|
||||
showInvalidMountPointScene();
|
||||
return;
|
||||
} else if (cause instanceof FileAlreadyExistsException) {
|
||||
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
|
||||
showInvalidMountPointScene();
|
||||
return;
|
||||
} else if (cause instanceof DirectoryNotEmptyException) {
|
||||
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
|
||||
showInvalidMountPointScene();
|
||||
return;
|
||||
} else {
|
||||
handleGenericError(impExc);
|
||||
}
|
||||
@@ -196,33 +118,44 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
|
||||
private void handleGenericError(Throwable e) {
|
||||
LOG.error("Unlock failed for technical reasons.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
|
||||
private void wipePassword(char[] pw) {
|
||||
if (pw != null) {
|
||||
Arrays.fill(pw, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
errorComponent.cause(e).window(window).build().showErrorScene();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
||||
|
||||
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
|
||||
case ASK -> Platform.runLater(() -> {
|
||||
window.setScene(successScene.get());
|
||||
window.show();
|
||||
});
|
||||
case REVEAL -> {
|
||||
Platform.runLater(window::close);
|
||||
vaultService.reveal(vault);
|
||||
}
|
||||
case IGNORE -> Platform.runLater(window::close);
|
||||
}
|
||||
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
|
||||
Throwable throwable = super.getException();
|
||||
if (throwable instanceof InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
} else {
|
||||
handleGenericError(throwable);
|
||||
}
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.cryptomator.ui.vaultoptions;
|
||||
import org.cryptomator.common.settings.WhenUnlocked;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.Observable;
|
||||
@@ -22,6 +23,7 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final HealthCheckComponent.Builder healthCheckWindow;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
public TextField vaultName;
|
||||
@@ -29,9 +31,10 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
|
||||
|
||||
@Inject
|
||||
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
|
||||
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.healthCheckWindow = healthCheckWindow;
|
||||
this.resourceBundle = resourceBundle;
|
||||
}
|
||||
|
||||
@@ -61,6 +64,12 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void showHealthCheck() {
|
||||
healthCheckWindow.vault(vault).build().showHealthCheckWindow();
|
||||
}
|
||||
|
||||
|
||||
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
|
||||
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class MasterkeyOptionsController implements FxController {
|
||||
this.changePasswordWindow = changePasswordWindow;
|
||||
this.recoveryKeyWindow = recoveryKeyWindow;
|
||||
this.keychain = keychain;
|
||||
if (keychain.isSupported()) {
|
||||
if (keychain.isSupported() && !keychain.isLocked()) {
|
||||
this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychain.getPassphraseStoredProperty(vault.getId()));
|
||||
} else {
|
||||
this.passwordSaved = new SimpleBooleanProperty(false);
|
||||
@@ -74,7 +74,7 @@ public class MasterkeyOptionsController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isPasswordSaved() {
|
||||
if (keychain.isSupported() && vault != null) {
|
||||
if (keychain.isSupported() && !keychain.isLocked() && vault != null) {
|
||||
return keychain.getPassphraseStoredProperty(vault.getId()).get();
|
||||
} else return false;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
alignment="CENTER_LEFT">
|
||||
<fx:define>
|
||||
<ToggleGroup fx:id="predefinedLocationToggler"/>
|
||||
<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
|
||||
<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
@@ -33,6 +35,8 @@
|
||||
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/>
|
||||
<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/>
|
||||
<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/>
|
||||
<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.locationPresets.foundMega}" managed="${controller.locationPresets.foundMega}"/>
|
||||
<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.locationPresets.foundPcloud}" managed="${controller.locationPresets.foundPcloud}"/>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
|
||||
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
|
||||
@@ -47,12 +51,8 @@
|
||||
|
||||
<VBox spacing="6">
|
||||
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
|
||||
<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" disable="true" HBox.hgrow="ALWAYS"/>
|
||||
<Label text="${controller.warningText}" wrapText="true" visible="${controller.showWarning}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
|
||||
<Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
|
||||
</VBox>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
</padding>
|
||||
<children>
|
||||
<fx:include source="/fxml/new_password.fxml"/>
|
||||
<fx:include fx:id="newPasswordScene" source="/fxml/new_password.fxml"/>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<Region prefHeight="12" VBox.vgrow="NEVER"/>
|
||||
|
||||
<fx:include source="/fxml/new_password.fxml"/>
|
||||
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
|
||||
|
||||
<CheckBox fx:id="finalConfirmationCheckbox" text="%changepassword.finalConfirmation" wrapText="true"/>
|
||||
|
||||
|
||||
24
main/ui/src/main/resources/fxml/health_check_details.fxml
Normal file
24
main/ui/src/main/resources/fxml/health_check_details.fxml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.CheckDetailController"
|
||||
prefWidth="500"
|
||||
spacing="6">
|
||||
<FormattedLabel fx:id="checkTitle" styleClass="label-large" format="%health.check.detail.header" arg1="${controller.taskName}"/>
|
||||
|
||||
<Label text="%health.check.detail.taskNotStarted" visible="${controller.taskNotStarted}" managed="${controller.taskNotStarted}"/>
|
||||
<Label text="%health.check.detail.taskRunning" visible="${controller.taskRunning}" managed="${controller.taskRunning}"/>
|
||||
<Label text="%health.check.detail.taskScheduled" visible="${controller.taskScheduled}" managed="${controller.taskScheduled}"/>
|
||||
<Label text="%health.check.detail.taskCancelled" visible="${controller.taskCancelled}" managed="${controller.taskCancelled}"/>
|
||||
<Label text="%health.check.detail.taskFailed" visible="${controller.taskFailed}" managed="${controller.taskFailed}"/>
|
||||
<FormattedLabel styleClass="label" format="%health.check.detail.taskSucceeded" arg1="${controller.taskDuration}" visible="${controller.taskSucceeded}" managed="${controller.taskSucceeded}"/>
|
||||
|
||||
<FormattedLabel styleClass="label" format="%health.check.detail.problemCount" arg1="${controller.countOfWarnSeverity}" arg2="${controller.countOfCritSeverity}" visible="${!controller.taskNotStarted}"
|
||||
managed="${!controller.taskNotStarted}" />
|
||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS"/>
|
||||
</VBox>
|
||||
46
main/ui/src/main/resources/fxml/health_check_list.fxml
Normal file
46
main/ui/src/main/resources/fxml/health_check_list.fxml
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import java.lang.Integer?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.CheckListController"
|
||||
minHeight="145"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<fx:define>
|
||||
<Integer fx:id="ZERO" fx:value="0"/>
|
||||
</fx:define>
|
||||
<children>
|
||||
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||
<VBox minWidth="80" maxWidth="200" spacing="6" HBox.hgrow="ALWAYS" >
|
||||
<Label fx:id="listHeading" text="%health.checkList.header"/>
|
||||
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
|
||||
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
|
||||
</VBox>
|
||||
<StackPane visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
|
||||
<VBox minWidth="300" alignment="CENTER" visible="${!controller.anyCheckSelected}" managed="${!controller.anyCheckSelected}" >
|
||||
<Label text="%health.check.detail.noSelectedCheck" wrapText="true" alignment="CENTER" />
|
||||
</VBox>
|
||||
<fx:include source="/fxml/health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" />
|
||||
</StackPane>
|
||||
</HBox>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" />
|
||||
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${!controller.finished}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" onAction="#exportResults"/>
|
||||
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.numberOfPickedChecks == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
</VBox>
|
||||
29
main/ui/src/main/resources/fxml/health_result_listcell.fxml
Normal file
29
main/ui/src/main/resources/fxml/health_result_listcell.fxml
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.ResultListCellController"
|
||||
prefHeight="25"
|
||||
prefWidth="200"
|
||||
spacing="6"
|
||||
alignment="CENTER_LEFT">
|
||||
<!-- Remark: Check the containing list view for a fixed cell size before editing height properties -->
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="6"/>
|
||||
</padding>
|
||||
<children>
|
||||
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16"/>
|
||||
<Label text="${controller.description}"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<!-- TODO: setting the minWidth of the button is just a workaround.
|
||||
What we actually want to do is to prevent shrinking the button more than the text
|
||||
-> own subclass of HBox is needed -->
|
||||
<Button fx:id="actionButton" text="%health.check.fixBtn" onAction="#runResultAction" alignment="CENTER" visible="false" minWidth="-Infinity"/>
|
||||
</children>
|
||||
</HBox>
|
||||
40
main/ui/src/main/resources/fxml/health_start.fxml
Normal file
40
main/ui/src/main/resources/fxml/health_start.fxml
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.StartController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Label text="%health.start.introduction" wrapText="true"/>
|
||||
|
||||
<!-- TODO: combine the two below labels to one and bind the properties accordingly or, preferably think about a new flow -->
|
||||
<Label text="%health.start.configInvalid" visible="${controller.invalidConfig}" managed="${controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red" />
|
||||
</graphic>
|
||||
</Label>
|
||||
<Label text="%health.start.configValid" visible="${!controller.invalidConfig}" managed="${!controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary" />
|
||||
</graphic>
|
||||
</Label>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${controller.invalidConfig}" defaultButton="true" onAction="#next"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -38,12 +38,12 @@
|
||||
<fx:include source="/fxml/preferences_updates.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="donationKeyTab" id="DONATION_KEY" text="%preferences.donationKey">
|
||||
<Tab fx:id="contributeTab" id="CONTRIBUTE" text="%preferences.contribute">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="HEART"/>
|
||||
</graphic>
|
||||
<content>
|
||||
<fx:include source="/fxml/preferences_donationkey.fxml"/>
|
||||
<fx:include source="/fxml/preferences_contribute.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController"
|
||||
fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController"
|
||||
spacing="18">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
@@ -24,7 +24,7 @@
|
||||
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
|
||||
<FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}">
|
||||
@@ -33,8 +33,8 @@
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<VBox HBox.hgrow="ALWAYS" spacing="6">
|
||||
<Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/>
|
||||
<Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT">
|
||||
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/>
|
||||
<Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="LINK"/>
|
||||
</graphic>
|
||||
@@ -43,6 +43,6 @@
|
||||
</HBox>
|
||||
</StackPane>
|
||||
|
||||
<TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
|
||||
<TextArea fx:id="supporterCertificateField" promptText="%preferences.contribute.promptText" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -23,7 +23,7 @@
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.general.theme"/>
|
||||
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
|
||||
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showDonationTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
|
||||
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<fx:include source="/fxml/new_password.fxml"/>
|
||||
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+I">
|
||||
<buttons>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
|
||||
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${controller.invalidNewPassword}"/>
|
||||
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${!controller.validPassword}"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.unlock.UnlockController"
|
||||
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.PassphraseEntryController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user