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/Constants.java b/main/commons/src/main/java/org/cryptomator/common/Constants.java index cbde9642a..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,6 +3,7 @@ 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 b273bb642..03aef5629 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 @@ -24,8 +24,6 @@ import java.nio.file.Path; import java.util.Objects; import java.util.Optional; import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; /** * The settings specific to a single vault. @@ -37,7 +35,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; private static final Random RNG = new Random(); @@ -52,7 +50,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 StringBinding mountName; @@ -63,7 +61,7 @@ public class VaultSettings { } Observable[] observables() { - return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock}; + return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock}; } public static VaultSettings withRandomId() { @@ -152,8 +150,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 04a352a49..d68a67e0b 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.endObject(); } @@ -46,7 +46,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; in.beginObject(); @@ -64,7 +64,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()); default -> { LOG.warn("Unsupported vault setting found in JSON: " + name); @@ -88,7 +88,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); return vaultSettings; } 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 9af77408c..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,13 +36,12 @@ 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() // @@ -49,8 +49,8 @@ public class FuseVolume extends AbstractVolume { .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); } } @@ -91,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(); @@ -102,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 a461d05cb..e36369c35 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 @@ -19,7 +19,6 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig; -import org.cryptomator.cryptofs.common.Constants; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.MasterkeyLoader; @@ -46,6 +45,7 @@ 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; @PerVault @@ -53,12 +53,13 @@ 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; @@ -76,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; @@ -105,26 +106,34 @@ public class Vault { 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().orElseThrow().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() // - .withKeyLoaders(keyLoader) // + .withKeyLoader(keyLoader) // .withFlags(flags) // - .withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) // - .withMaxNameLength(vaultSettings.filenameLengthLimit().get()) // + .withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) // .build(); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); } - private void destroyCryptoFileSystem(CryptoFileSystem fs) { + private void destroyCryptoFileSystem() { LOG.trace("Trying to close associated CryptoFS..."); + CryptoFileSystem fs = cryptoFileSystem.getAndSet(null); if (fs != null) { try { fs.close(); @@ -143,23 +152,42 @@ public class Vault { try { cryptoFileSystem.set(fs); volume = volumeProvider.get(); - volume.mount(fs, getEffectiveMountFlags()); + volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit); success = true; } finally { if (!success) { - destroyCryptoFileSystem(fs); + 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(); } - CryptoFileSystem fs = cryptoFileSystem.getAndSet(null); - destroyCryptoFileSystem(fs); + + //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 { @@ -170,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() { @@ -199,7 +223,7 @@ public class Vault { } public boolean isLocked() { - return state.get() == VaultState.LOCKED; + return state.get() == VaultState.Value.LOCKED; } public BooleanBinding processingProperty() { @@ -207,7 +231,7 @@ public class Vault { } public boolean isProcessing() { - return state.get() == VaultState.PROCESSING; + return state.get() == VaultState.Value.PROCESSING; } public BooleanBinding unlockedProperty() { @@ -215,7 +239,7 @@ public class Vault { } public boolean isUnlocked() { - return state.get() == VaultState.UNLOCKED; + return state.get() == VaultState.Value.UNLOCKED; } public BooleanBinding missingProperty() { @@ -223,7 +247,7 @@ public class Vault { } public boolean isMissing() { - return state.get() == VaultState.MISSING; + return state.get() == VaultState.Value.MISSING; } public BooleanBinding needsMigrationProperty() { @@ -231,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() { @@ -239,7 +263,7 @@ public class Vault { } public boolean isUnknownError() { - return state.get() == VaultState.ERROR; + return state.get() == VaultState.Value.ERROR; } public StringBinding displayNameProperty() { @@ -255,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 { 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 d71b20462..39a175169 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 @@ -25,7 +25,6 @@ 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; @@ -95,42 +94,43 @@ 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(VaultState.Value.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); + VaultState.Value 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(VaultState.Value.ERROR); vault.setLastKnownException(e); - yield VaultState.ERROR; + yield VaultState.Value.ERROR; } } case ERROR, UNLOCKED, PROCESSING -> previousState; }; } - private static VaultState determineVaultState(Path pathToVault) throws IOException { + private static VaultState.Value determineVaultState(Path pathToVault) throws IOException { if (!CryptoFileSystemProvider.containsVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { - return VaultState.MISSING; + return VaultState.Value.MISSING; } else if (Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { - return VaultState.NEEDS_MIGRATION; + return VaultState.Value.NEEDS_MIGRATION; } else { - return VaultState.LOCKED; + return VaultState.Value.LOCKED; } } 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 b5c0ca533..19d577975 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 @@ -8,12 +8,10 @@ package org.cryptomator.common.vaults; import dagger.Module; import dagger.Provides; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.Constants; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.cryptofs.CryptoFileSystem; -import org.cryptomator.cryptofs.VaultConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,11 +24,9 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; @Module @@ -44,12 +40,6 @@ public class VaultModule { return new AtomicReference<>(); } - @Provides - @PerVault - public ObjectProperty provideVaultState(VaultState initialState) { - return new SimpleObjectProperty<>(initialState); - } - @Provides @Named("lastKnownException") @PerVault 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 293302bd2..7c2dae811 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -25,17 +25,17 @@ 16 - 2.1.0-beta3 + 2.1.0-beta4 1.0.0-beta2 1.0.0-beta2 1.0.0-beta2 1.0.0-beta1 - 1.3.0 - 1.2.4 + 1.3.1 + 1.3.0 1.2.0 - 15 + 16 3.11 3.13.0 2.1.0 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 b37ffc80c..b55c0df60 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 @@ -8,13 +8,14 @@ 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.cryptolib.common.MasterkeyFileLoader; 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.Tasks; +import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +37,7 @@ 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; @@ -53,6 +55,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; @@ -174,23 +177,22 @@ public class CreateNewVaultPasswordController implements FxController { Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME); try (Masterkey masterkey = Masterkey.generate(csprng)) { masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase); - } - // 2. initialize vault: - var context = new StaticMasterkeyFileLoaderContext(masterkeyFilePath, passphrase); - var loader = masterkeyFileAccess.keyLoader(path, context); - try { - CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoaders(loader).build(); - CryptoFileSystemProvider.initialize(path, fsProps, MasterkeyFileLoader.keyId(MASTERKEY_FILENAME)); + // 2. initialize vault: + try { + MasterkeyLoader loader = ignored -> masterkey; + 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())); + // 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); } - } catch (CryptoException e) { - throw new IOException("Failed initialize vault.", e); } // 4. write vault-external readme file: 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/addvaultwizard/StaticMasterkeyFileLoaderContext.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/StaticMasterkeyFileLoaderContext.java deleted file mode 100644 index 13c3b05b1..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/StaticMasterkeyFileLoaderContext.java +++ /dev/null @@ -1,28 +0,0 @@ -package org.cryptomator.ui.addvaultwizard; - -import com.google.common.base.Preconditions; -import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext; - -import java.nio.file.Path; - -class StaticMasterkeyFileLoaderContext implements MasterkeyFileLoaderContext { - - private final Path masterkeyFilePath; - private final CharSequence passphrase; - - StaticMasterkeyFileLoaderContext(Path masterkeyFilePath, CharSequence passphrase) { - this.masterkeyFilePath = masterkeyFilePath; - this.passphrase = passphrase; - } - - @Override - public Path getCorrectMasterkeyFilePath(String s) { - return masterkeyFilePath; - } - - @Override - public CharSequence getPassphrase(Path path) { - Preconditions.checkArgument(masterkeyFilePath.equals(path)); - return passphrase; - } -} 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 93c7e0576..b0ad164e2 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 @@ -31,13 +31,13 @@ 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 public class ChangePasswordController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class); - private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; private final Stage window; private final Vault vault; 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/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index 917e704fc..b0ea8da47 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 @@ -5,11 +5,13 @@ import org.cryptomator.common.LicenseHolder; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.vaults.Vault; +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; @@ -44,8 +46,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; @@ -55,13 +58,14 @@ public class FxApplication extends Application { private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; @Inject - FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWindowBuilderProvider, Provider lockWindowBuilderProvider, Lazy quitWindow, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { + FxApplication(Settings settings, Lazy mainWindow, Lazy preferencesWindow, Provider unlockWorkflowBuilderProvider, Provider lockWorkflowBuilderProvider, Lazy quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional trayIntegration, Optional appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) { 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; @@ -113,15 +117,23 @@ 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."))); + } }); } 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/health/StartController.java b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java index 170493c97..a614da59c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java @@ -122,7 +122,7 @@ public class StartController implements FxController { private void loadKey() { assert !Platform.isFxApplicationThread(); assert unverifiedVaultConfig.isPresent(); - try (var masterkey = keyLoadingStrategy.masterkeyLoader().loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) { + try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) { var unverifiedCfg = unverifiedVaultConfig.get(); var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion()); vaultConfigRef.set(verifiedCfg); 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 index fc274d2a7..190bf0e1f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java @@ -3,8 +3,11 @@ 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}) 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 index 6621c5e92..15a5d27b8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -35,13 +35,13 @@ abstract class KeyLoadingModule { @Provides @KeyLoading @KeyLoadingScoped - static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional keyId, Map strategies) { + static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional keyId, Map> strategies) { if (keyId.isEmpty()) { return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided")); } else { String scheme = keyId.get().getScheme(); var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme)); - return strategies.getOrDefault(scheme, fallback); + return strategies.getOrDefault(scheme, () -> fallback).get(); } } 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 index 80f65b6bc..ed8ca0540 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java @@ -1,21 +1,34 @@ package org.cryptomator.ui.keyloading; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -public interface KeyLoadingStrategy { +import java.net.URI; + +/** + * A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions. + */ +@FunctionalInterface +public interface KeyLoadingStrategy extends MasterkeyLoader { /** - * @return A reusable masterkey loader, preconfigured with the vault of the current unlock process - * @throws MasterkeyLoadingFailedException If unable to provide the masterkey loader - */ - MasterkeyLoader masterkeyLoader() throws MasterkeyLoadingFailedException; - - /** - * Allows the component to try and recover from an exception thrown while loading a masterkey. + * 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 exception An exception thrown either by {@link #masterkeyLoader()} or by the returned {@link MasterkeyLoader}. - * @return true if this component was able to handle the exception and another attempt should be made to load a masterkey + * @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; @@ -38,7 +51,7 @@ public interface KeyLoadingStrategy { * @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}. */ static KeyLoadingStrategy failed(Exception exception) { - return () -> { + return keyid -> { if (exception instanceof MasterkeyLoadingFailedException e) { throw e; } else { diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java deleted file mode 100644 index 7445d581d..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingContext.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.cryptomator.ui.keyloading.masterkeyfile; - -import dagger.Lazy; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; -import org.cryptomator.cryptolib.common.MasterkeyFileLoaderContext; -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.KeyLoadingScoped; -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.nio.CharBuffer; -import java.nio.file.Path; -import java.util.concurrent.atomic.AtomicReference; - -@KeyLoadingScoped -class MasterkeyFileLoadingContext implements MasterkeyFileLoaderContext { - - 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 boolean wrongPassword; - - @Inject - public MasterkeyFileLoadingContext(@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) { - this.window = window; - this.passphraseEntryScene = passphraseEntryScene; - this.selectMasterkeyFileScene = selectMasterkeyFileScene; - this.passwordEntryLock = passwordEntryLock; - this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; - this.password = password; - this.filePath = filePath; - } - - @Override - public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) { - if (filePath.get() != null) { // e.g. already chosen on previous attempt with wrong password - return filePath.get(); - } - - assert filePath.get() == null; - try { - if (askForCorrectMasterkeyFile() == MasterkeyFileLoadingModule.MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) { - return filePath.get(); - } else { - throw new UnlockCancelledException("Choosing masterkey file cancelled."); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new UnlockCancelledException("Choosing masterkey file interrupted", e); - } - } - - private MasterkeyFileLoadingModule.MasterkeyFileProvision askForCorrectMasterkeyFile() 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(); - } - - @Override - public CharSequence getPassphrase(Path path) throws UnlockCancelledException { - if (password.get() != null) { // e.g. pre-filled from keychain - return CharBuffer.wrap(password.get()); - } - - assert password.get() == null; - try { - if (askForPassphrase() == MasterkeyFileLoadingModule.PasswordEntry.PASSWORD_ENTERED) { - assert password.get() != null; - return CharBuffer.wrap(password.get()); - } else { - throw new UnlockCancelledException("Password entry cancelled."); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new UnlockCancelledException("Password entry interrupted", e); - } - } - - 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(); - } - - 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 - } - } -} 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 index f872cb697..8eda41cd0 100644 --- 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 @@ -24,7 +24,7 @@ class MasterkeyFileLoadingFinisher { private final Vault vault; private final Optional storedPassword; private final AtomicReference enteredPassword; - private final boolean shouldSavePassword; + private final AtomicBoolean shouldSavePassword; private final KeychainManager keychain; @Inject @@ -32,12 +32,12 @@ class MasterkeyFileLoadingFinisher { this.vault = vault; this.storedPassword = storedPassword; this.enteredPassword = enteredPassword; - this.shouldSavePassword = shouldSavePassword.get(); + this.shouldSavePassword = shouldSavePassword; this.keychain = keychain; } public void cleanup(boolean successfullyUnlocked) { - if (successfullyUnlocked && shouldSavePassword) { + if (successfullyUnlocked && shouldSavePassword.get()) { savePasswordToSystemkeychain(); } wipePassword(storedPassword.orElse(null)); 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 index 31b123f53..d9413121c 100644 --- 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 @@ -7,8 +7,6 @@ import dagger.multibindings.IntoMap; import dagger.multibindings.StringKey; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptolib.common.MasterkeyFileAccess; -import org.cryptomator.cryptolib.common.MasterkeyFileLoader; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; @@ -45,12 +43,6 @@ public abstract class MasterkeyFileLoadingModule { CANCELED } - @Provides - @KeyLoadingScoped - static MasterkeyFileLoader provideMasterkeyFileLoader(MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Vault vault, MasterkeyFileLoadingContext context) { - return masterkeyFileAccess.keyLoader(vault.getPath(), context); - } - @Provides @KeyLoadingScoped static UserInteractionLock providePasswordEntryLock() { @@ -125,7 +117,7 @@ public abstract class MasterkeyFileLoadingModule { @Binds @IntoMap @KeyLoadingScoped - @StringKey("masterkeyfile") + @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 index ae6694198..464671929 100644 --- 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 @@ -1,38 +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.MasterkeyFileLoader; +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 -class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { +public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { - private final MasterkeyFileLoader masterkeyFileLoader; - private final MasterkeyFileLoadingContext context; + 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(MasterkeyFileLoader masterkeyFileLoader, MasterkeyFileLoadingContext context, MasterkeyFileLoadingFinisher finisher) { - this.masterkeyFileLoader = masterkeyFileLoader; - this.context = context; + 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 MasterkeyFileLoader masterkeyLoader() { - return masterkeyFileLoader; + 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) { - return context.recoverFromException(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 == 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/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/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/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 index b0866d80f..145618fa8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListContextMenuController.java @@ -14,19 +14,13 @@ import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import javax.inject.Inject; import javafx.beans.binding.Binding; -import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.fxml.FXML; import javafx.stage.Stage; -import java.util.Arrays; import java.util.EnumSet; import java.util.Optional; -import static org.cryptomator.common.vaults.VaultState.ERROR; -import static org.cryptomator.common.vaults.VaultState.LOCKED; -import static org.cryptomator.common.vaults.VaultState.MISSING; -import static org.cryptomator.common.vaults.VaultState.NEEDS_MIGRATION; -import static org.cryptomator.common.vaults.VaultState.UNLOCKED; +import static org.cryptomator.common.vaults.VaultState.Value.*; @MainWindowScoped public class VaultListContextMenuController implements FxController { @@ -37,7 +31,7 @@ public class VaultListContextMenuController implements FxController { private final KeychainManager keychain; private final RemoveVaultComponent.Builder removeVault; private final VaultOptionsComponent.Builder vaultOptionsWindow; - private final OptionalBinding selectedVaultState; + private final OptionalBinding selectedVaultState; private final Binding selectedVaultPassphraseStored; private final Binding selectedVaultRemovable; private final Binding selectedVaultUnlockable; @@ -57,7 +51,6 @@ public class VaultListContextMenuController implements FxController { 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) { 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 04ea9f152..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,9 +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 javax.inject.Inject; import javafx.beans.binding.Bindings; @@ -15,26 +17,39 @@ 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 final Stage mainWindow; private final ObservableList vaults; private final ObjectProperty selectedVault; private final VaultListCellFactory cellFactory; private final AddVaultWizardComponent.Builder addVaultWizard; private final BooleanBinding emptyVaultList; + private final RemoveVaultComponent.Builder removeVaultDialogue; public ListView vaultList; @Inject - VaultListController(ObservableList vaults, ObjectProperty selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard) { + 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.removeVaultDialogue = removeVaultDialogue; this.emptyVaultList = Bindings.isEmpty(vaults); @@ -59,6 +74,28 @@ public class VaultListController implements FxController { 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) { @@ -80,6 +117,13 @@ public class VaultListController implements FxController { addVaultWizard.build().showAddVaultWizard(); } + 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(); + } + } + // Getter and Setter public BooleanBinding emptyVaultListProperty() { 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 ff18510d9..4789a8044 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; @@ -90,7 +91,10 @@ 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())); } @FXML @@ -102,7 +106,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(() -> { @@ -116,10 +120,10 @@ public class MigrationRunController implements FxController { }).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()); } @@ -128,20 +132,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/RecoveryKeyFactory.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java index ee159e072..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 @@ -14,28 +14,21 @@ import java.io.IOException; 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 java.util.Arrays; import java.util.Collection; -import java.util.Optional; +import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; -import static org.cryptomator.common.Constants.PEPPER; @Singleton public class RecoveryKeyFactory { - private static final String MASTERKEY_BACKUP_SUFFIX = ".bkup"; - private final WordEncoder wordEncoder; - private final SecureRandom csprng; private final MasterkeyFileAccess masterkeyFileAccess; @Inject - public RecoveryKeyFactory(WordEncoder wordEncoder, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { + public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) { this.wordEncoder = wordEncoder; - this.csprng = csprng; this.masterkeyFileAccess = masterkeyFileAccess; } @@ -92,6 +85,7 @@ public class RecoveryKeyFactory { 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); } 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/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 06b42155e..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 @@ -54,22 +54,12 @@ public class UnlockWorkflow extends Task { this.invalidMountPointScene = invalidMountPointScene; this.errorComponent = errorComponent; this.keyLoadingStrategy = keyLoadingStrategy; - - setOnFailed(event -> { - Throwable throwable = event.getSource().getException(); - if (throwable instanceof InvalidMountPointException e) { - handleInvalidMountPoint(e); - } else { - handleGenericError(throwable); - } - }); } @Override protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException { try { attemptUnlock(); - handleSuccess(); return true; } catch (UnlockCancelledException e) { cancel(false); // set Tasks state to cancelled @@ -80,7 +70,7 @@ public class UnlockWorkflow extends Task { private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException { boolean success = false; try { - vault.unlock(keyLoadingStrategy.masterkeyLoader()); + vault.unlock(keyLoadingStrategy); success = true; } catch (MasterkeyLoadingFailedException e) { if (keyLoadingStrategy.recoverFromException(e)) { @@ -94,21 +84,6 @@ public class UnlockWorkflow extends Task { } } - private void handleSuccess() { - 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); - } - } - private void handleInvalidMountPoint(InvalidMountPointException impExc) { MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!", impExc)).getMountPointRequirement(); assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible @@ -143,27 +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(); - } - - @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/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 @@ + +