diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 773454e39..8a464f599 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore index 900a21ae0..d0e6b59a3 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,6 @@ pom.xml.versionsBackup .idea/compiler.xml .idea/encodings.xml .idea/jarRepositories.xml +.idea/uiDesigner.xml .idea/**/libraries/ *.iml \ No newline at end of file diff --git a/README.md b/README.md index d5cf5ea8e..dccec1091 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index 2a0a42ecb..f8e0e8078 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -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") diff --git a/main/commons/src/main/java/org/cryptomator/common/Constants.java b/main/commons/src/main/java/org/cryptomator/common/Constants.java index 432cd08e5..06dedfc2d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/Constants.java +++ b/main/commons/src/main/java/org/cryptomator/common/Constants.java @@ -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]; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 1b93d0901..1db67d4d5 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -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 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 actionAfterUnlock() { diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index ffff021e2..26aa3d51c 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -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); diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java index a33dbd5ee..c998761d0 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java @@ -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 choosers) { + public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable 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 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); } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java index 5400f8ff2..a1579fdaf 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java @@ -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 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 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(); diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java b/main/commons/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java new file mode 100644 index 000000000..237630da0 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/LockNotCompletedException.java @@ -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); + } +} diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index 98a6cf21b..02fd03600 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -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 volumeProvider; private final StringBinding defaultMountFlags; private final AtomicReference cryptoFileSystem; - private final ObjectProperty state; + private final VaultState state; private final ObjectProperty 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 volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference cryptoFileSystem, ObjectProperty state, @Named("lastKnownException") ObjectProperty lastKnownException, VaultStats stats) { + Vault(VaultSettings vaultSettings, Provider volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty 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 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 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 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}; } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java index debfab3ed..47be62520 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultComponent.java @@ -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); diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index b7c10a4d4..d5038630a 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -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 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; + }; } } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java index 56a2814cf..901ee7f42 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java @@ -40,12 +40,6 @@ public class VaultModule { return new AtomicReference<>(); } - @Provides - @PerVault - public ObjectProperty 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 diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java index fa5e6c295..801ea7653 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultState.java @@ -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 implements ObservableObjectValue { + + 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; + 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 fromState to toState. + * + * @param fromState Previous state + * @param toState New state + * @return true 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); + } + } } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java index 46cf31991..6dc86e8be 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java @@ -26,7 +26,7 @@ public class VaultStats { private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class); private final AtomicReference fs; - private final ObjectProperty state; + private final VaultState state; private final ScheduledService> 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 fs, ObjectProperty state, ExecutorService executor) { + VaultStats(AtomicReference 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()); } } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java index 74a307d5a..f608122bf 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Volume.java @@ -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 onExitAction) throws IOException, VolumeException, InvalidMountPointException; /** * Reveals the mounted volume. diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java b/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java index 8b4f27fdb..03c83377c 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/WebDavVolume.java @@ -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 onExitAction; @Inject public WebDavVolume(Provider 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 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 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 diff --git a/main/pom.xml b/main/pom.xml index 1e48da8a7..fdefe694b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -25,29 +25,29 @@ 16 - 1.9.14 + 2.1.0-beta5 1.0.0-beta2 1.0.0-beta2 1.0.0-beta2 1.0.0-beta1 - 1.2.9 - 1.2.4 - 1.1.4 + 1.3.1 + 1.3.1 + 1.2.2 - 15 + 16 3.11 - 3.12.0 + 3.15.0 2.1.0 - 30.0-jre - 2.32 + 30.1.1-jre + 2.35.1 2.8.6 1.7.30 1.2.3 - 5.7.0 - 3.6.0 + 5.7.1 + 3.9.0 2.2 @@ -218,12 +218,6 @@ test - - - com.fasterxml.jackson.core - jackson-databind - 2.10.5.1 - diff --git a/main/suppression.xml b/main/suppression.xml index 6fe12f417..43acf2e8d 100644 --- a/main/suppression.xml +++ b/main/suppression.xml @@ -1,11 +1,6 @@ - - - com.fasterxml.jackson.core:jackson-databind:2.10.5.1 - CVE-2020-25649 - ^org\.cryptomator:fuse-nio-adapter:.*$ diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java index 42d243e80..8a5a776ea 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java @@ -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 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 password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } @Binds diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java index a5398d51b..214ed19b0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java @@ -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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 4eeea5c3d..39e25d503 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -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 chooseNameScene; private final Lazy choosePasswordScene; - private final ErrorComponent.Builder errorComponent; private final LocationPresets locationPresets; private final ObjectProperty 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 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 chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, ErrorComponent.Builder errorComponent, LocationPresets locationPresets, ObjectProperty vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { + CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, LocationPresets locationPresets, ObjectProperty 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 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 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 statusGraphicProperty() { + return statusGraphic; + } + + public Node getStatusGraphic() { + return statusGraphic.get(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index c4e226359..4b4e02ed2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -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 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 password; private final ReadmeGenerator readmeGenerator; + private final SecureRandom csprng; + private final MasterkeyFileAccess masterkeyFileAccess; private final BooleanProperty processing; private final BooleanProperty readyToCreateVault; private final ObjectBinding 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 chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator) { + CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty 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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java index b9f796043..6cec655cb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/LocationPresets.java @@ -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 iclouddriveLocation; private final ReadOnlyObjectProperty dropboxLocation; private final ReadOnlyObjectProperty gdriveLocation; private final ReadOnlyObjectProperty onedriveLocation; + private final ReadOnlyObjectProperty megaLocation; + private final ReadOnlyObjectProperty 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 megaLocationProperty() { + return megaLocation; + } + + public Path getMegaLocation() { + return megaLocation.get(); + } + + public BooleanBinding foundMegaProperty() { + return foundMega; + } + + public boolean isFoundMega() { + return foundMega.get(); + } + + public ReadOnlyObjectProperty pcloudLocationProperty() { + return pcloudLocation; + } + + public Path getPcloudLocation() { + return pcloudLocation.get(); + } + + public BooleanBinding foundPcloudProperty() { + return foundPcloud; + } + + public boolean isFoundPcloud() { + return foundPcloud.get(); + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index 52cfe2b81..ccc0184ac 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -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 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 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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java index e80871208..d95b19410 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java @@ -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 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 password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 4d429ab7d..b8d5bbff0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -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"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java index b7bf48870..13e59f2cd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java @@ -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 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 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() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java index 57393b858..b81ddec49 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java @@ -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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 1c799d56d..91865c02e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -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"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java index c99cc62ea..04ed7e477 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java @@ -12,6 +12,7 @@ public class FormattedLabel extends Label { private final StringProperty format = new SimpleStringProperty(""); private final ObjectProperty arg1 = new SimpleObjectProperty<>(); + private final ObjectProperty 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 arg2Property() { + return arg2; + } + + public Object getArg2() { + return arg2.get(); + } + + public void setArg2(Object arg2) { + this.arg2.set(arg2); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java index 0fde886f6..3689b7e6e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java @@ -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)); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index cc6e97d72..a19a13bf1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -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 mainWindow; private final Lazy preferencesWindow; private final Lazy quitWindow; - private final Provider unlockWindowBuilderProvider; - private final Provider lockWindowBuilderProvider; + private final Provider unlockWorkflowBuilderProvider; + private final Provider lockWorkflowBuilderProvider; + private final ErrorComponent.Builder errorWindowBuilder; private final Optional trayIntegration; private final Optional appearanceProvider; private final VaultService vaultService; @@ -60,13 +63,14 @@ public class FxApplication extends Application { private final ScheduledExecutorService scheduledExecutorService; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Provider lockWindowBuilderProvider, Lazy quitWindow, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWorkflowBuilderProvider, Provider lockWorkflowBuilderProvider, Lazy quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional trayIntegration, Optional 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 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 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 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."))); + } }); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java index 596e704e9..ef8621f3b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/UpdateCheckerModule.java @@ -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 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 provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding period) { + static ScheduledService provideCheckForUpdatesService(ExecutorService executor, Optional httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding period) { ScheduledService service = new ScheduledService<>() { @Override protected Task 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())); diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java b/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java new file mode 100644 index 000000000..f3968c27d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java @@ -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 { + + private final Iterator remainingTasks; + + @Inject + public BatchService(Iterable tasks) { + this.remainingTasks = tasks.iterator(); + } + + @Override + protected Task createTask() { + Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks"); + return remainingTasks.next(); + } + + @Override + protected void succeeded() { + if (remainingTasks.hasNext()) { + this.restart(); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java new file mode 100644 index 000000000..d579ff709 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java @@ -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 results; + private final OptionalBinding taskState; + private final Binding taskName; + private final Binding taskDuration; + private final ResultListCellFactory resultListCellFactory; + private final Binding taskRunning; + private final Binding taskScheduled; + private final Binding taskFinished; + private final Binding taskNotStarted; + private final Binding taskSucceeded; + private final Binding taskFailed; + private final Binding taskCancelled; + private final Binding countOfWarnSeverity; + private final Binding countOfCritSeverity; + + public ListView resultsListView; + private Subscription resultSubscription; + + @Inject + public CheckDetailController(ObjectProperty 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 observable, HealthCheckTask oldValue, HealthCheckTask newValue) { + if (resultSubscription != null) { + resultSubscription.unsubscribe(); + } + if (newValue != null) { + resultSubscription = EasyBind.bindContent(results, newValue.results()); + } + } + + private Function, 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 taskNameProperty() { + return taskName; + } + + public Number getTaskDuration() { + return taskDuration.getValue(); + } + + public Binding taskDurationProperty() { + return taskDuration; + } + + public long getCountOfWarnSeverity() { + return countOfWarnSeverity.getValue().longValue(); + } + + public Binding countOfWarnSeverityProperty() { + return countOfWarnSeverity; + } + + public long getCountOfCritSeverity() { + return countOfCritSeverity.getValue().longValue(); + } + + public Binding countOfCritSeverityProperty() { + return countOfCritSeverity; + } + + public boolean isTaskRunning() { + return taskRunning.getValue(); + } + + public Binding taskRunningProperty() { + return taskRunning; + } + + public boolean isTaskFinished() { + return taskFinished.getValue(); + } + + public Binding taskFinishedProperty() { + return taskFinished; + } + + public boolean isTaskScheduled() { + return taskScheduled.getValue(); + } + + public Binding taskScheduledProperty() { + return taskScheduled; + } + + public boolean isTaskNotStarted() { + return taskNotStarted.getValue(); + } + + public Binding taskNotStartedProperty() { + return taskNotStarted; + } + + public boolean isTaskSucceeded() { + return taskSucceeded.getValue(); + } + + public Binding taskSucceededProperty() { + return taskSucceeded; + } + + public boolean isTaskFailed() { + return taskFailed.getValue(); + } + + public Binding taskFailedProperty() { + return taskFailed; + } + + public boolean isTaskCancelled() { + return taskCancelled.getValue(); + } + + public Binding taskCancelledProperty() { + return taskCancelled; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java new file mode 100644 index 000000000..78f8b1b33 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java @@ -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 { + + 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 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; + }; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java new file mode 100644 index 000000000..ccb41d56b --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java @@ -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 END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED); + + private final Stage window; + private final ObservableList tasks; + private final ReportWriter reportWriter; + private final ExecutorService executorService; + private final ObjectProperty selectedTask; + private final Lazy errorComponenBuilder; + private final SimpleObjectProperty> runningTask; + private final Binding running; + private final Binding finished; + private final Map listPickIndicators; + private final IntegerProperty numberOfPickedChecks; + private final BooleanBinding anyCheckSelected; + private final BooleanProperty showResultScreen; + + /* FXML */ + public ListView checksListView; + + + @Inject + public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy 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() { + @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 runningProperty() { + return running; + } + + public boolean isFinished() { + return finished.getValue(); + } + + public Binding 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; + } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java new file mode 100644 index 000000000..48b16f694 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -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(); + + 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java new file mode 100644 index 000000000..e33a9f2f1 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -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 provideMasterkeyRef() { + return new AtomicReference<>(); + } + + @Provides + @HealthCheckScoped + static AtomicReference provideVaultConfigRef() { + return new AtomicReference<>(); + } + + @Provides + @HealthCheckScoped + static Collection provideAvailableHealthChecks() { + return HealthCheck.allChecks(); + } + + @Provides + @HealthCheckScoped + static ObjectProperty provideSelectedHealthCheckTask() { + return new SimpleObjectProperty<>(); + } + + /* Only inject with Lazy-Wrapper!*/ + @Provides + @HealthCheckScoped + static Collection provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference 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, Provider> 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 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 provideWindowShowingChangeListener(AtomicReference 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); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java new file mode 100644 index 000000000..af563d737 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java new file mode 100644 index 000000000..7acbfc1c2 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java @@ -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 { + + 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 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 results() { + return results; + } + + public HealthCheck getCheck() { + return check; + } + + public LongProperty durationInMillisProperty() { + return durationInMillis; + } + + public long getDurationInMillis() { + return durationInMillis.get(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java new file mode 100644 index 000000000..50243c07a --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java new file mode 100644 index 000000000..fb74cbd51 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java @@ -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 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 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()); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java new file mode 100644 index 000000000..9a639e8a0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java @@ -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 masterkeyRef, AtomicReference 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 + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java new file mode 100644 index 000000000..f2bca059a --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java @@ -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 result; + private final Binding 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 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 resultProperty() { + return result; + } + + public String getDescription() { + return description.getValue(); + } + + public Binding descriptionProperty() { + return description; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java new file mode 100644 index 000000000..7acada487 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java @@ -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, ListCell> { + + private final FxmlLoaderFactory fxmlLoaders; + + @Inject + ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + this.fxmlLoaders = fxmlLoaders; + } + + @Override + public ListCell call(ListView 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 { + + 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); + } + } + } +} \ No newline at end of file diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java new file mode 100644 index 000000000..3e9cc5b07 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java @@ -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 unverifiedVaultConfig; + private final KeyLoadingStrategy keyLoadingStrategy; + private final ExecutorService executor; + private final AtomicReference masterkeyRef; + private final AtomicReference vaultConfigRef; + private final Lazy checkScene; + private final Lazy errorComponent; + + /* FXML */ + + @Inject + public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java new file mode 100644 index 000000000..51f9302c8 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java new file mode 100644 index 000000000..190bf0e1f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java @@ -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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java new file mode 100644 index 000000000..f7eb8922f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -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, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @KeyLoading + @KeyLoadingScoped + static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> 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); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java new file mode 100644 index 000000000..5f4355f96 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java new file mode 100644 index 000000000..ed8ca0540 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java @@ -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. + *

+ * 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 true 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 true 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); + } + }; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java new file mode 100644 index 000000000..8eda41cd0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java @@ -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 storedPassword; + private final AtomicReference enteredPassword; + private final AtomicBoolean shouldSavePassword; + private final KeychainManager keychain; + + @Inject + MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference 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, ' '); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java new file mode 100644 index 000000000..901eacfb9 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -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 providePasswordEntryLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @KeyLoadingScoped + static UserInteractionLock provideMasterkeyFileProvisionLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @Named("savedPassword") + @KeyLoadingScoped + static Optional 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 provideUserProvidedMasterkeyPath() { + return new AtomicReference<>(); + } + + @Provides + @KeyLoadingScoped + static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { + return new AtomicReference<>(storedPassword.orElse(null)); + } + + @Provides + @Named("savePassword") + @KeyLoadingScoped + static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional 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); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java new file mode 100644 index 000000000..f8fbdd720 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -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 passphraseEntryScene; + private final Lazy selectMasterkeyFileScene; + private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock masterkeyFileProvisionLock; + private final AtomicReference password; + private final AtomicReference 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 passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java similarity index 88% rename from main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java rename to main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index 674b16142..d4356be85 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -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 password; private final AtomicBoolean savePassword; private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock passwordEntryLock; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; private final ObjectBinding unlockButtonContentDisplay; @@ -66,7 +69,7 @@ public class UnlockController implements FxController { public Animation unlockAnimation; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock 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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java new file mode 100644 index 000000000..39be2b36e --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java @@ -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 masterkeyPath; + private final UserInteractionLock masterkeyFileProvisionLock; + private final ResourceBundle resourceBundle; + + @Inject + public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock 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); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java index 0ab11d374..49ef79e83 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java +++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java @@ -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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java index 646c3ce6b..75af409f1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java +++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLifecycleListener.java @@ -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 STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR); + public static final Set 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); } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java index acb6d1355..87fd486f2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -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 { private final UserInteractionLock forceLockDecisionLock; private final Lazy lockForcedScene; private final Lazy lockFailedScene; + private final ErrorComponent.Builder errorComponent; @Inject - public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene) { + public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy 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 { 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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 65c3a8fcf..392671246 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index f8316d289..90311bd5b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -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) diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java index 5a5bb59a6..ea9311c88 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowTitleController.java @@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController { @FXML public void showDonationKeyPreferences() { - application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY); + application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java index 40df6b7c5..7b5c4c7c9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailController.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java index bc2f2100a..5fee2e6d1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java @@ -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, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) { + VaultDetailLockedController(ObjectProperty 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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java index abc287d6e..f18369e7a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListCellController.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java new file mode 100644 index 000000000..145618fa8 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java @@ -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 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 selectedVaultState; + private final Binding selectedVaultPassphraseStored; + private final Binding selectedVaultRemovable; + private final Binding selectedVaultUnlockable; + private final Binding selectedVaultLockable; + + @Inject + VaultListContextMenuController(ObjectProperty 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 selectedVaultUnlockableProperty() { + return selectedVaultUnlockable; + } + + public boolean isSelectedVaultUnlockable() { + return selectedVaultUnlockable.getValue(); + } + + public Binding selectedVaultLockableProperty() { + return selectedVaultLockable; + } + + public boolean isSelectedVaultLockable() { + return selectedVaultLockable.getValue(); + } + + public Binding selectedVaultRemovableProperty() { + return selectedVaultRemovable; + } + + public boolean isSelectedVaultRemovable() { + return selectedVaultRemovable.getValue(); + } + + public Binding selectedVaultPassphraseStoredProperty() { + return selectedVaultPassphraseStored; + } + + public boolean isSelectedVaultPassphraseStored() { + return selectedVaultPassphraseStored.getValue(); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java index fc751d862..dbe858a55 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java @@ -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 vaults; private final ObjectProperty 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 vaultList; @Inject - VaultListController(ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) { + VaultListController(@MainWindow Stage mainWindow, ObservableList vaults, ObjectProperty 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 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(); - } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index 830e15819..a9b0085d8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -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); diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index ea1fbfe3a..64d71a8b7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java index 64d5c991b..276794753 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesController.java @@ -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; diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java index 51fa6b581..5fee567b7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java @@ -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 diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java index a76f2ac1c..892d16a8c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java @@ -22,9 +22,9 @@ public enum SelectedPreferencesTab { UPDATES, /** - * Show donation key tab + * Show contribute tab */ - DONATION_KEY, + CONTRIBUTE, /** * Show about tab diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/SupporterCertificateController.java similarity index 68% rename from main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java rename to main/ui/src/main/java/org/cryptomator/ui/preferences/SupporterCertificateController.java index a4814ec82..02b8bab91 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/DonationKeyPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/SupporterCertificateController.java @@ -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() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 902f2cd50..104794604 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -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()); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java index 15d4e651e..c078c718b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java @@ -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 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); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 58d9fbde9..b30167e73 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -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 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 password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index cdf54990f..a2319ba3c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -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 newPassword; private final Lazy 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 newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene, ErrorComponent.Builder errorComponent) { + public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy 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(); } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java index 803de314c..6f195274c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/stats/VaultStatisticsModule.java @@ -43,8 +43,8 @@ abstract class VaultStatisticsModule { var weakStage = new WeakReference<>(stage); vault.stateProperty().addListener(new ChangeListener<>() { @Override - public void changed(ObservableValue observable, VaultState oldValue, VaultState newValue) { - if (newValue != VaultState.UNLOCKED) { + public void changed(ObservableValue observable, VaultState.Value oldValue, VaultState.Value newValue) { + if (newValue != VaultState.Value.UNLOCKED) { Stage stage = weakStage.get(); if (stage != null) { stage.hide(); diff --git a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 96529c5fb..4ce3808c4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -44,6 +44,9 @@ class TrayMenuController { public void initTrayMenu() { vaults.addListener(this::vaultListChanged); + vaults.forEach(v -> { + v.displayNameProperty().addListener(this::vaultListChanged); + }); rebuildMenu(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java new file mode 100644 index 000000000..c795eb5ea --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java @@ -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); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java index 1850953c9..fd85db988 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -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 unlockScene; private final Vault vault; @Inject - UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy 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 */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 2ed17fdff..3c1267e65 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -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 providePasswordEntryLock() { - return new UserInteractionLock<>(null); - } - - @Provides - @Named("savedPassword") - @UnlockScoped - static Optional 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 providePassword(@Named("savedPassword") Optional storedPassword) { - return new AtomicReference(storedPassword.orElse(null)); - } - - @Provides - @Named("savePassword") - @UnlockScoped - static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional 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) diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index c1b5fbd45..36c3eacf9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -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 { private final Stage window; private final Vault vault; private final VaultService vaultService; - private final AtomicReference password; - private final AtomicBoolean savePassword; - private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; - private final KeychainManager keychain; - private final Lazy unlockScene; private final Lazy successScene; private final Lazy invalidMountPointScene; private final ErrorComponent.Builder errorComponent; + private final KeyLoadingStrategy keyLoadingStrategy; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) { + UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy 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 { 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 { 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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index e860ee811..79d3b53ad 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -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 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 { private final ResourceBundle resourceBundle; diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java index ee38e1f18..fb9d6b711 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java @@ -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; } diff --git a/main/ui/src/main/resources/fxml/addvault_new_location.fxml b/main/ui/src/main/resources/fxml/addvault_new_location.fxml index 66b34ff95..3c25ba569 100644 --- a/main/ui/src/main/resources/fxml/addvault_new_location.fxml +++ b/main/ui/src/main/resources/fxml/addvault_new_location.fxml @@ -20,6 +20,8 @@ alignment="CENTER_LEFT"> + + @@ -33,6 +35,8 @@ + +