~exprez135/cryptomator-libre

aef33dc8646f2f17e0d7dd30deeb3e135e26e382 — Sebastian Stenzel 1 year, 4 months ago 9145f5d + 06d2f2d 1.5.4
Merge branch 'release/1.5.4'
87 files changed, 968 insertions(+), 609 deletions(-)

M main/buildkit/pom.xml
M main/commons/pom.xml
M main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java
M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java
M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java
M main/commons/src/main/java/org/cryptomator/common/settings/VolumeImpl.java
M main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java
A main/commons/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java
M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java
M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java
M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java
M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java
M main/keychain/pom.xml
M main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java
M main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java
M main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java
M main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
M main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
M main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java
M main/launcher/pom.xml
M main/pom.xml
M main/ui/pom.xml
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java
M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java
A main/ui/src/main/java/org/cryptomator/ui/common/StageFactory.java
A main/ui/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java
M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
M main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
M main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java
M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
M main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java
M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
M main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java
M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java
A main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
M main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
M main/ui/src/main/resources/fxml/preferences_general.fxml
M main/ui/src/main/resources/fxml/unlock.fxml
M main/ui/src/main/resources/fxml/unlock_success.fxml
M main/ui/src/main/resources/fxml/vault_options_general.fxml
M main/ui/src/main/resources/i18n/strings.properties
M main/ui/src/main/resources/i18n/strings_ar.properties
M main/ui/src/main/resources/i18n/strings_ca.properties
M main/ui/src/main/resources/i18n/strings_cs.properties
M main/ui/src/main/resources/i18n/strings_de.properties
M main/ui/src/main/resources/i18n/strings_es.properties
M main/ui/src/main/resources/i18n/strings_fr.properties
M main/ui/src/main/resources/i18n/strings_hi.properties
M main/ui/src/main/resources/i18n/strings_it.properties
M main/ui/src/main/resources/i18n/strings_ja.properties
M main/ui/src/main/resources/i18n/strings_ko.properties
M main/ui/src/main/resources/i18n/strings_lv.properties
M main/ui/src/main/resources/i18n/strings_nb.properties
M main/ui/src/main/resources/i18n/strings_nl.properties
M main/ui/src/main/resources/i18n/strings_nn.properties
M main/ui/src/main/resources/i18n/strings_pt_BR.properties
M main/ui/src/main/resources/i18n/strings_ru.properties
M main/ui/src/main/resources/i18n/strings_sv.properties
M main/ui/src/main/resources/i18n/strings_tr.properties
M main/ui/src/main/resources/i18n/strings_zh.properties
M main/ui/src/main/resources/i18n/strings_zh_TW.properties
M main/ui/src/main/resources/license/THIRD-PARTY.txt
M main/ui/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
M main/buildkit/pom.xml => main/buildkit/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.3</version>
		<version>1.5.4</version>
	</parent>
	<artifactId>buildkit</artifactId>
	<packaging>pom</packaging>

M main/commons/pom.xml => main/commons/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.3</version>
		<version>1.5.4</version>
	</parent>
	<artifactId>commons</artifactId>
	<name>Cryptomator Commons</name>

M main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java => main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +14 -38
@@ 58,46 58,22 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
		while (in.hasNext()) {
			String name = in.nextName();
			switch (name) {
				case "directories":
					settings.getDirectories().addAll(readVaultSettingsArray(in));
					break;
				case "askedForUpdateCheck":
					settings.askedForUpdateCheck().set(in.nextBoolean());
					break;
				case "checkForUpdatesEnabled":
					settings.checkForUpdates().set(in.nextBoolean());
					break;
				case "startHidden":
					settings.startHidden().set(in.nextBoolean());
					break;
				case "port":
					settings.port().set(in.nextInt());
					break;
				case "numTrayNotifications":
					settings.numTrayNotifications().set(in.nextInt());
					break;
				case "preferredGvfsScheme":
					settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
					break;
				case "debugMode":
					settings.debugMode().set(in.nextBoolean());
					break;
				case "preferredVolumeImpl":
					settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
					break;
				case "theme":
					settings.theme().set(parseUiTheme(in.nextString()));
					break;
				case "uiOrientation":
					settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
					break;
				case "licenseKey":
					settings.licenseKey().set(in.nextString());
					break;
				default:
				case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
				case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
				case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
				case "startHidden" -> settings.startHidden().set(in.nextBoolean());
				case "port" -> settings.port().set(in.nextInt());
				case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
				case "preferredGvfsScheme" -> settings.preferredGvfsScheme().set(parseWebDavUrlSchemePrefix(in.nextString()));
				case "debugMode" -> settings.debugMode().set(in.nextBoolean());
				case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString()));
				case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
				case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
				case "licenseKey" -> settings.licenseKey().set(in.nextString());
				default -> {
					LOG.warn("Unsupported vault setting found in JSON: " + name);
					in.skipValue();
					break;
				}
			}
		}
		in.endObject();

M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java => main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +20 -11
@@ 26,7 26,6 @@ import java.util.Random;

/**
 * The settings specific to a single vault.
 * TODO: Change the name of individualMountPath and its derivatives to customMountPath
 */
public class VaultSettings {



@@ 36,6 35,7 @@ public class VaultSettings {
	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 WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
	
	private static final Random RNG = new Random(); 



@@ 45,11 45,12 @@ public class VaultSettings {
	private final StringProperty winDriveLetter = new SimpleStringProperty();
	private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
	private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT);
	private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
	private final StringProperty individualMountPath = new SimpleStringProperty();
	private final BooleanProperty useCustomMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH);
	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 ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);

	public VaultSettings(String id) {
		this.id = Objects.requireNonNull(id);


@@ 58,7 59,7 @@ public class VaultSettings {
	}

	Observable[] observables() {
		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit};
		return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
	}

	private void deriveMountNameFromPath(Path path) {


@@ 122,17 123,17 @@ public class VaultSettings {
		return revealAfterMount;
	}

	public BooleanProperty usesIndividualMountPath() {
		return usesIndividualMountPath;
	public BooleanProperty useCustomMountPath() {
		return useCustomMountPath;
	}

	public StringProperty individualMountPath() {
		return individualMountPath;
	public StringProperty customMountPath() {
		return customMountPath;
	}

	public Optional<String> getIndividualMountPath() {
		if (usesIndividualMountPath.get()) {
			return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get()));
	public Optional<String> getCustomMountPath() {
		if (useCustomMountPath.get()) {
			return Optional.ofNullable(Strings.emptyToNull(customMountPath.get()));
		} else {
			return Optional.empty();
		}


@@ 150,6 151,14 @@ public class VaultSettings {
		return filenameLengthLimit;
	}

	public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
		return actionAfterUnlock;
	}

	public WhenUnlocked getActionAfterUnlock() {
		return actionAfterUnlock.get();
	}

	/* Hashcode/Equals */

	@Override

M main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java => main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +32 -41
@@ 25,11 25,12 @@ class VaultSettingsJsonAdapter {
		out.name("winDriveLetter").value(value.winDriveLetter().get());
		out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
		out.name("revealAfterMount").value(value.revealAfterMount().get());
		out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get());
		out.name("individualMountPath").value(value.individualMountPath().get());
		out.name("useCustomMountPath").value(value.useCustomMountPath().get());
		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("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
		out.endObject();
	}



@@ 37,56 38,36 @@ class VaultSettingsJsonAdapter {
		String id = null;
		String path = null;
		String mountName = null;
		String individualMountPath = null;
		String customMountPath = null;
		String winDriveLetter = null;
		boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
		boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT;
		boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
		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;
		WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;

		in.beginObject();
		while (in.hasNext()) {
			String name = in.nextName();
			switch (name) {
				case "id":
					id = in.nextString();
					break;
				case "path":
					path = in.nextString();
					break;
				case "mountName":
					mountName = in.nextString();
					break;
				case "winDriveLetter":
					winDriveLetter = in.nextString();
					break;
				case "unlockAfterStartup":
					unlockAfterStartup = in.nextBoolean();
					break;
				case "revealAfterMount":
					revealAfterMount = in.nextBoolean();
					break;
				case "usesIndividualMountPath":
					usesIndividualMountPath = in.nextBoolean();
					break;
				case "individualMountPath":
					individualMountPath = in.nextString();
					break;
				case "usesReadOnlyMode":
					usesReadOnlyMode = in.nextBoolean();
					break;
				case "mountFlags":
					mountFlags = in.nextString();
					break;
				case "filenameLengthLimit":
					filenameLengthLimit = in.nextInt();
					break;
				default:
				case "id" -> id = in.nextString();
				case "path" -> path = in.nextString();
				case "mountName" -> mountName = in.nextString();
				case "winDriveLetter" -> winDriveLetter = in.nextString();
				case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
				case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
				case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
				case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
				case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
				case "mountFlags" -> mountFlags = in.nextString();
				case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
				case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
				default -> {
					LOG.warn("Unsupported vault setting found in JSON: " + name);
					in.skipValue();
					break;
				}
			}
		}
		in.endObject();


@@ 97,12 78,22 @@ class VaultSettingsJsonAdapter {
		vaultSettings.winDriveLetter().set(winDriveLetter);
		vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
		vaultSettings.revealAfterMount().set(revealAfterMount);
		vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath);
		vaultSettings.individualMountPath().set(individualMountPath);
		vaultSettings.useCustomMountPath().set(useCustomMountPath);
		vaultSettings.customMountPath().set(customMountPath);
		vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
		vaultSettings.mountFlags().set(mountFlags);
		vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
		vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
		return vaultSettings;
	}

	private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
		try {
			return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
		} catch (IllegalArgumentException e) {
			LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
			return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
		}
	}

}

M main/commons/src/main/java/org/cryptomator/common/settings/VolumeImpl.java => main/commons/src/main/java/org/cryptomator/common/settings/VolumeImpl.java +0 -16
@@ 1,7 1,5 @@
package org.cryptomator.common.settings;

import java.util.Arrays;

public enum VolumeImpl {
	WEBDAV("WebDAV"),
	FUSE("FUSE"),


@@ 17,18 15,4 @@ public enum VolumeImpl {
		return displayName;
	}

	/**
	 * Finds a VolumeImpl by display name.
	 *
	 * @param displayName Display name of the VolumeImpl
	 * @return VolumeImpl with the given <code>displayName</code>.
	 * @throws IllegalArgumentException if not volumeImpl with the given <code>displayName</code> was found.
	 */
	public static VolumeImpl forDisplayName(String displayName) throws IllegalArgumentException {
		return Arrays.stream(values()) //
				.filter(impl -> impl.displayName.equals(displayName)) //
				.findAny() //
				.orElseThrow(IllegalArgumentException::new);
	}

}

M main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java => main/commons/src/main/java/org/cryptomator/common/settings/WebDavUrlScheme.java +0 -16
@@ 1,7 1,5 @@
package org.cryptomator.common.settings;

import java.util.Arrays;

public enum WebDavUrlScheme {
	DAV("dav", "dav:// (Gnome, Nautilus, ...)"),
	WEBDAV("webdav", "webdav:// (KDE, Dolphin, ...)");


@@ 20,18 18,4 @@ public enum WebDavUrlScheme {
	public String getDisplayName() {
		return displayName;
	}

	/**
	 * Finds a WebDavUrlScheme by prefix.
	 *
	 * @param prefix Prefix of the WebDavUrlScheme
	 * @return WebDavUrlScheme with the given <code>prefix</code>.
	 * @throws IllegalArgumentException if not WebDavUrlScheme with the given <code>prefix</code> was found.
	 */
	public static WebDavUrlScheme forPrefix(String prefix) throws IllegalArgumentException {
		return Arrays.stream(values()) //
				.filter(impl -> impl.prefix.equals(prefix)) //
				.findAny() //
				.orElseThrow(IllegalArgumentException::new);
	}
}

A main/commons/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java => main/commons/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java +17 -0
@@ 0,0 1,17 @@
package org.cryptomator.common.settings;

public enum WhenUnlocked {
	IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
	REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
	ASK("vaultOptions.general.actionAfterUnlock.ask");

	private String displayName;

	WhenUnlocked(String displayName) {
		this.displayName = displayName;
	}

	public String getDisplayName() {
		return displayName;
	}
}

M main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/DokanyVolume.java +2 -2
@@ 51,7 51,7 @@ public class DokanyVolume implements Volume {
		try {
			this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, mountName, FS_TYPE_NAME, mountFlags.strip());
		} catch (MountFailedException e) {
			if (vaultSettings.getIndividualMountPath().isPresent()) {
			if (vaultSettings.getCustomMountPath().isPresent()) {
				LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
			}
			throw new VolumeException("Unable to mount Filesystem", e);


@@ 59,7 59,7 @@ public class DokanyVolume implements Volume {
	}

	private Path determineMountPoint() throws VolumeException, IOException {
		Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
		Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
		if (optionalCustomMountPoint.isPresent()) {
			Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
			checkProvidedMountPoint(customMountPoint);

M main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java => main/commons/src/main/java/org/cryptomator/common/vaults/FuseVolume.java +1 -1
@@ 45,7 45,7 @@ public class FuseVolume implements Volume {

	@Override
	public void mount(CryptoFileSystem fs, String mountFlags) throws IOException, FuseNotSupportedException, VolumeException {
		Optional<String> optionalCustomMountPoint = vaultSettings.getIndividualMountPath();
		Optional<String> optionalCustomMountPoint = vaultSettings.getCustomMountPath();
		if (optionalCustomMountPoint.isPresent()) {
			Path customMountPoint = Paths.get(optionalCustomMountPoint.get());
			checkProvidedMountPoint(customMountPoint);

M main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java => main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +1 -1
@@ 121,7 121,7 @@ public class Vault {
	}

	public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException {
		if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) {
		if (vaultSettings.useCustomMountPath().get() && Strings.isNullOrEmpty(vaultSettings.customMountPath().get())) {
			throw new NotDirectoryException("");
		}
		CryptoFileSystem fs = getCryptoFileSystem(passphrase);

M main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java => main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +20 -1
@@ 92,8 92,27 @@ public class VaultListManager {
		}
		return compBuilder.build().vault();
	}
	
	public static VaultState redetermineVaultState(Vault vault) {
		VaultState previousState = vault.getState();
		return switch (previousState) {
			case LOCKED, NEEDS_MIGRATION, MISSING -> {
				try {
					VaultState determinedState = determineVaultState(vault.getPath());
					vault.setState(determinedState);
					yield determinedState;
				} catch (IOException e) {
					LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
					vault.setState(VaultState.ERROR);
					vault.setLastKnownException(e);
					yield VaultState.ERROR;
				}
			}
			case ERROR, UNLOCKED, PROCESSING -> previousState;
		};
	}

	public static VaultState determineVaultState(Path pathToVault) throws IOException {
	private static VaultState determineVaultState(Path pathToVault) throws IOException {
		if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
			return VaultState.MISSING;
		} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {

M main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java => main/commons/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +1 -2
@@ 16,7 16,6 @@ import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Paths;
import java.util.Arrays;

public class VaultSettingsJsonAdapterTest {



@@ 32,7 31,7 @@ public class VaultSettingsJsonAdapterTest {
		Assertions.assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get());
		Assertions.assertEquals("test", vaultSettings.mountName().get());
		Assertions.assertEquals("X", vaultSettings.winDriveLetter().get());
		Assertions.assertEquals("/home/test/crypto", vaultSettings.individualMountPath().get());
		Assertions.assertEquals("/home/test/crypto", vaultSettings.customMountPath().get());
		Assertions.assertEquals("--foo --bar", vaultSettings.mountFlags().get());



M main/keychain/pom.xml => main/keychain/pom.xml +1 -2
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.3</version>
		<version>1.5.4</version>
	</parent>
	<artifactId>keychain</artifactId>
	<name>System Keychain Access</name>


@@ 40,7 40,6 @@
		<dependency>
			<groupId>de.swiesend</groupId>
			<artifactId>secret-service</artifactId>
			<version>1.0.0-RC.3</version>
		</dependency>

		<!-- Logging -->

M main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java +7 -0
@@ 28,4 28,11 @@ public interface KeychainAccess {
	 */
	void deletePassphrase(String key) throws KeychainAccessException;

	/**
	 * Updates a passphrase with a given key.
	 *
	 * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
	 * @param passphrase The secret to be updated in this keychain.
	 */
	void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;
}

M main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccess.java +5 -0
@@ 48,4 48,9 @@ public class LinuxSecretServiceKeychainAccess implements KeychainAccessStrategy 
	public void deletePassphrase(String key) throws KeychainAccessException {
		delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		delegate.orElseThrow(IllegalStateException::new).changePassphrase(key, passphrase);
	}
}

M main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java => main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java +15 -1
@@ 9,6 9,8 @@ import java.util.Map;

class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {

	private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator";

	@Override
	public boolean isSupported() {
		try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {


@@ 24,7 26,7 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list == null) {
				keyring.createItem("Cryptomator", passphrase, createAttributes(key));
				keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);


@@ 57,6 59,18 @@ class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {
		}
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list != null) {
				keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);
		}
	}

	private Map<String, String> createAttributes(String key) {
		Map<String, String> attributes = new HashMap();
		attributes.put("Vault", key);

M main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +5 -0
@@ 46,4 46,9 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy {
		keychain().deletePassword(key);
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		storePassphrase(key, passphrase);
	}

}

M main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java +5 -0
@@ 113,6 113,11 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		storePassphrase(key, passphrase);
	}

	@Override
	public boolean isSupported() {
		return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();
	}

M main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java => main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java +6 -0
@@ 32,6 32,12 @@ class MapKeychainAccess implements KeychainAccessStrategy {
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) {
		map.get(key);
		storePassphrase(key, passphrase);
	}

	@Override
	public boolean isSupported() {
		return true;
	}

M main/launcher/pom.xml => main/launcher/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.3</version>
		<version>1.5.4</version>
	</parent>
	<artifactId>launcher</artifactId>
	<name>Cryptomator Launcher</name>

M main/pom.xml => main/pom.xml +10 -2
@@ 3,7 3,7 @@
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.cryptomator</groupId>
	<artifactId>main</artifactId>
	<version>1.5.3</version>
	<version>1.5.4</version>
	<packaging>pom</packaging>
	<name>Cryptomator</name>



@@ 27,12 27,13 @@
		<cryptomator.cryptofs.version>1.9.10</cryptomator.cryptofs.version>
		<cryptomator.jni.version>2.2.2</cryptomator.jni.version>
		<cryptomator.fuse.version>1.2.3</cryptomator.fuse.version>
		<cryptomator.dokany.version>1.1.14</cryptomator.dokany.version>
		<cryptomator.dokany.version>1.1.15</cryptomator.dokany.version>
		<cryptomator.webdav.version>1.0.11</cryptomator.webdav.version>

		<!-- 3rd party dependencies -->
		<javafx.version>14</javafx.version>
		<commons-lang3.version>3.9</commons-lang3.version>
		<secret-service.version>1.0.0</secret-service.version>
		<jwt.version>3.10.2</jwt.version>
		<easybind.version>1.0.3</easybind.version>
		<guava.version>28.2-jre</guava.version>


@@ 160,6 161,13 @@
				<artifactId>commons-lang3</artifactId>
				<version>${commons-lang3.version}</version>
			</dependency>

			<!-- Linux System Keychain -->
			<dependency>
				<groupId>de.swiesend</groupId>
				<artifactId>secret-service</artifactId>
				<version>${secret-service.version}</version>
			</dependency>
			
			<!-- JWT -->
			<dependency>

M main/ui/pom.xml => main/ui/pom.xml +1 -1
@@ 4,7 4,7 @@
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.5.3</version>
		<version>1.5.4</version>
	</parent>
	<artifactId>ui</artifactId>
	<name>Cryptomator GUI</name>

M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java => main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java +3 -3
@@ 21,6 21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyDisplayController;



@@ 51,13 52,12 @@ public abstract class AddVaultModule {
	@Provides
	@AddVaultWizardWindow
	@AddVaultWizardScoped
	static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("addvaultwizard.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java => main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java +1 -1
@@ 27,7 27,7 @@ public class AddVaultSuccessController implements FxController {
	@FXML
	public void unlockAndClose() {
		close();
		fxApplication.showUnlockWindow(vault.get());
		fxApplication.startUnlockWorkflow(vault.get());
	}

	@FXML

M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java => main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +20 -10
@@ 2,32 2,28 @@ package org.cryptomator.ui.changepassword;

import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.util.ResourceBundle;
import java.nio.CharBuffer;
import java.util.Optional;

import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;



@@ 40,17 36,19 @@ public class ChangePasswordController implements FxController {
	private final Vault vault;
	private final ObjectProperty<CharSequence> newPassword;
	private final ErrorComponent.Builder errorComponent;
	private final Optional<KeychainAccess> keychain;

	public NiceSecurePasswordField oldPasswordField;
	public CheckBox finalConfirmationCheckbox;
	public Button finishButton;

	@Inject
	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent) {
	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, Optional<KeychainAccess> keychain) {
		this.window = window;
		this.vault = vault;
		this.newPassword = newPassword;
		this.errorComponent = errorComponent;
		this.keychain = keychain;
	}

	@FXML


@@ 69,8 67,9 @@ public class ChangePasswordController implements FxController {
	public void finish() {
		try {
			CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
			LOG.info("Successful changed password for {}", vault.getDisplayableName());
			LOG.info("Successfully changed password for {}", vault.getDisplayableName());
			window.close();
			updatePasswordInSystemkeychain();
		} catch (IOException e) {
			LOG.error("IO error occured during password change. Unable to perform operation.", e);
			errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();


@@ 81,6 80,17 @@ public class ChangePasswordController implements FxController {
		}
	}

	private void updatePasswordInSystemkeychain() {
		if (keychain.isPresent()) {
			try {
				keychain.get().changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
				LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayableName());
			} catch (KeychainAccessException e) {
				LOG.error("Failed to update password in system keychain.", e);
			}
		}
	}

	/* Getter/Setter */

	public Vault getVault() {

M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java => main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java +3 -3
@@ 18,6 18,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;

import javax.inject.Named;
import javax.inject.Provider;


@@ 46,13 47,12 @@ abstract class ChangePasswordModule {
	@Provides
	@ChangePasswordWindow
	@ChangePasswordScoped
	static Stage provideStage(@Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @Named("changePasswordOwner") Stage owner, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("changepassword.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


A main/ui/src/main/java/org/cryptomator/ui/common/StageFactory.java => main/ui/src/main/java/org/cryptomator/ui/common/StageFactory.java +26 -0
@@ 0,0 1,26 @@
package org.cryptomator.ui.common;

import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.util.function.Consumer;

public class StageFactory {

	private final Consumer<Stage> initializer;

	public StageFactory(Consumer<Stage> initializer) {
		this.initializer = initializer;
	}
	
	public Stage create() {
		return create(StageStyle.DECORATED);
	}
	
	public Stage create(StageStyle stageStyle) {
		Stage stage = new Stage(stageStyle);
		initializer.accept(stage);
		return stage;
	}

}

A main/ui/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java => main/ui/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java +51 -0
@@ 0,0 1,51 @@
package org.cryptomator.ui.common;

import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UserInteractionLock<E extends Enum> {

	private final Lock lock = new ReentrantLock();
	private final Condition condition = lock.newCondition();
	private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
	private volatile E state;
	
	public UserInteractionLock(E initialValue) {
		state = initialValue;
	}
	
	public void interacted(E result) {
		assert Platform.isFxApplicationThread();
		lock.lock();
		try {
			state = result;
			awaitingInteraction.set(false);
			condition.signal();
		} finally {
			lock.unlock();
		}
	}
	
	public E awaitInteraction() throws InterruptedException {
		assert !Platform.isFxApplicationThread();
		lock.lock();
		try {
			Platform.runLater(() -> awaitingInteraction.set(true));
			condition.await();
			return state;
		} finally {
			lock.unlock();
		}
	}
	
	public ReadOnlyBooleanProperty awaitingInteraction() {
		return awaitingInteraction;
	}

}

M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java => main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +0 -166
@@ 53,62 53,6 @@ public class VaultService {
	}

	/**
	 * Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
	 *
	 * @param vaults The vaults to unlock
	 * @implNote No-op if no system keychain is present
	 */
	public void attemptAutoUnlock(Collection<Vault> vaults) {
		if (!keychain.isPresent()) {
			LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
		} else {
			List<Task<Vault>> unlockTasks = vaults.stream().map(v -> createAutoUnlockTask(v, keychain.get())).collect(Collectors.toList());
			Task<Collection<Vault>> runSequentiallyTask = new RunSequentiallyTask(unlockTasks);
			executorService.execute(runSequentiallyTask);
		}
	}

	/**
	 * Creates but doesn't start an auto-unlock task.
	 *
	 * @param vault The vault to unlock
	 * @param keychainAccess The system keychain holding the passphrase for the vault
	 * @return The task
	 */
	public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
		Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
		task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
		task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
		return task;
	}

	/**
	 * Unlocks a vault in a background thread
	 *
	 * @param vault The vault to unlock
	 * @param passphrase The password to use - wipe this param asap
	 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
	 */
	public void unlock(Vault vault, CharSequence passphrase) {
		executorService.execute(createUnlockTask(vault, passphrase));
	}

	/**
	 * Creates but doesn't start an unlock task.
	 *
	 * @param vault The vault to unlock
	 * @param passphrase The password to use - wipe this param asap
	 * @return The task
	 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
	 */
	public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
		Task<Vault> task = new UnlockVaultTask(vault, passphrase);
		task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
		task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
		return task;
	}

	/**
	 * Locks a vault in a background thread.
	 *
	 * @param vault The vault to lock


@@ 210,116 154,6 @@ public class VaultService {
	}

	/**
	 * A task that runs a list of tasks in their given order
	 */
	private static class RunSequentiallyTask extends Task<Collection<Vault>> {

		private final List<Task<Vault>> tasks;

		public RunSequentiallyTask(List<Task<Vault>> tasks) {
			this.tasks = List.copyOf(tasks);
		}

		@Override
		protected List<Vault> call() throws ExecutionException, InterruptedException {
			List<Vault> completed = new ArrayList<>();
			for (Task<Vault> task : tasks) {
				task.run();
				Vault done = task.get();
				completed.add(done);
			}
			return completed;
		}
	}

	private static class AutoUnlockVaultTask extends Task<Vault> {

		private final Vault vault;
		private final KeychainAccess keychain;

		public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
			this.vault = vault;
			this.keychain = keychain;
		}

		@Override
		protected Vault call() throws Exception {
			char[] storedPw = null;
			try {
				storedPw = keychain.loadPassphrase(vault.getId());
				if (storedPw == null) {
					throw new InvalidPassphraseException();
				}
				vault.unlock(CharBuffer.wrap(storedPw));
			} finally {
				if (storedPw != null) {
					Arrays.fill(storedPw, ' ');
				}
			}
			return vault;
		}

		@Override
		protected void scheduled() {
			vault.setState(VaultState.PROCESSING);
		}

		@Override
		protected void succeeded() {
			vault.setState(VaultState.UNLOCKED);
		}

		@Override
		protected void failed() {
			vault.setState(VaultState.LOCKED);
		}
	}

	private static class UnlockVaultTask extends Task<Vault> {

		private final Vault vault;
		private final CharBuffer passphrase;

		/**
		 * @param vault The vault to unlock
		 * @param passphrase The password to use - wipe this param asap
		 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
		 */
		public UnlockVaultTask(Vault vault, CharSequence passphrase) {
			this.vault = vault;
			this.passphrase = CharBuffer.allocate(passphrase.length());
			for (int i = 0; i < passphrase.length(); i++) {
				this.passphrase.put(i, passphrase.charAt(i));
			}
		}

		@Override
		protected Vault call() throws Exception {
			try {
				vault.unlock(passphrase);
			} finally {
				Arrays.fill(passphrase.array(), ' ');
			}
			return vault;
		}

		@Override
		protected void scheduled() {
			vault.setState(VaultState.PROCESSING);
		}

		@Override
		protected void succeeded() {
			vault.setState(VaultState.UNLOCKED);
		}

		@Override
		protected void failed() {
			vault.setState(VaultState.LOCKED);
		}
	}

	/**
	 * A task that locks a vault
	 */
	private static class LockVaultTask extends Task<Vault> {

M main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java => main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java +2 -2
@@ 94,8 94,8 @@ public class NiceSecurePasswordField extends StackPane {
		passwordField.setPassword(password);
	}

	public void swipe() {
		passwordField.swipe();
	public void wipe() {
		passwordField.wipe();
	}

	public void selectAll() {

M main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java => main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +10 -10
@@ 40,7 40,7 @@ import java.util.Arrays;
 */
public class SecurePasswordField extends TextField {

	private static final char SWIPE_CHAR = ' ';
	private static final char WIPE_CHAR = ' ';
	private static final int INITIAL_BUFFER_SIZE = 50;
	private static final int GROW_BUFFER_SIZE = 50;
	private static final String DEFAULT_PLACEHOLDER = "●";


@@ 103,7 103,7 @@ public class SecurePasswordField extends TextField {
		if (e.getCode() == KeyCode.CAPS) {
			updateCapsLocked();
		} else if (SHORTCUT_BACKSPACE.match(e)) {
			swipe();
			wipe();
		}
	}



@@ 189,7 189,7 @@ public class SecurePasswordField extends TextField {
		if (length > content.length) {
			char[] newContent = new char[length + GROW_BUFFER_SIZE];
			System.arraycopy(content, 0, newContent, 0, content.length);
			swipe(content);
			wipe(content);
			this.content = newContent;
		}
	}


@@ 201,7 201,7 @@ public class SecurePasswordField extends TextField {
	 * @implNote The CharSequence will not copy the backing char[].
	 * Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
	 * @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
	 * @see #swipe()
	 * @see #wipe()
	 */
	@Override
	public CharSequence getCharacters() {


@@ 220,7 220,7 @@ public class SecurePasswordField extends TextField {
			buf[i] = password.charAt(i);
		}
		setPassword(buf);
		Arrays.fill(buf, SWIPE_CHAR);
		Arrays.fill(buf, WIPE_CHAR);
	}

	/**


@@ 231,7 231,7 @@ public class SecurePasswordField extends TextField {
	 * @param password
	 */
	public void setPassword(char[] password) {
		swipe();
		wipe();
		content = Arrays.copyOf(password, password.length);
		length = password.length;



@@ 242,14 242,14 @@ public class SecurePasswordField extends TextField {
	/**
	 * Destroys the stored password by overriding each character with a different character.
	 */
	public void swipe() {
		swipe(content);
	public void wipe() {
		wipe(content);
		length = 0;
		setText(null);
	}

	private void swipe(char[] buffer) {
		Arrays.fill(buffer, SWIPE_CHAR);
	private void wipe(char[] buffer) {
		Arrays.fill(buffer, WIPE_CHAR);
	}

	/* Observable Properties */

M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java => main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordModule.java +3 -3
@@ 17,6 17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;

import javax.inject.Named;
import javax.inject.Provider;


@@ 37,13 38,12 @@ abstract class ForgetPasswordModule {
	@Provides
	@ForgetPasswordWindow
	@ForgetPasswordScoped
	static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("forgetPasswordOwner") Stage owner) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("forgetPasswordOwner") Stage owner) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("forgetPassword.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +17 -26
@@ 27,6 27,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Provider;
import java.awt.desktop.QuitResponse;
import java.util.Optional;



@@ 38,24 39,24 @@ public class FxApplication extends Application {
	private final Settings settings;
	private final Lazy<MainWindowComponent> mainWindow;
	private final Lazy<PreferencesComponent> preferencesWindow;
	private final UnlockComponent.Builder unlockWindowBuilder;
	private final QuitComponent.Builder quitWindowBuilder;
	private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
	private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
	private final Optional<MacFunctions> macFunctions;
	private final VaultService vaultService;
	private final LicenseHolder licenseHolder;
	private final ObservableSet<Stage> visibleStages = FXCollections.observableSet();
	private final BooleanBinding hasVisibleStages = Bindings.isNotEmpty(visibleStages);
	private final BooleanBinding hasVisibleStages;

	@Inject
	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder) {
	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
		this.settings = settings;
		this.mainWindow = mainWindow;
		this.preferencesWindow = preferencesWindow;
		this.unlockWindowBuilder = unlockWindowBuilder;
		this.quitWindowBuilder = quitWindowBuilder;
		this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
		this.quitWindowBuilderProvider = quitWindowBuilderProvider;
		this.macFunctions = macFunctions;
		this.vaultService = vaultService;
		this.licenseHolder = licenseHolder;
		this.hasVisibleStages = Bindings.isNotEmpty(visibleStages);
	}

	public void start() {


@@ 73,11 74,6 @@ public class FxApplication extends Application {
		throw new UnsupportedOperationException("Use start() instead.");
	}

	private void addVisibleStage(Stage stage) {
		visibleStages.add(stage);
		stage.setOnHidden(evt -> visibleStages.remove(stage));
	}

	private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
		if (newValue) {
			macFunctions.map(MacFunctions::uiState).ifPresent(MacApplicationUiState::transformToForegroundApplication);


@@ 88,32 84,28 @@ public class FxApplication extends Application {

	public void showPreferencesWindow(SelectedPreferencesTab selectedTab) {
		Platform.runLater(() -> {
			Stage stage = preferencesWindow.get().showPreferencesWindow(selectedTab);
			addVisibleStage(stage);
			preferencesWindow.get().showPreferencesWindow(selectedTab);
			LOG.debug("Showing Preferences");
		});
	}

	public void showMainWindow() {
		Platform.runLater(() -> {
			Stage stage = mainWindow.get().showMainWindow();
			addVisibleStage(stage);
			mainWindow.get().showMainWindow();
			LOG.debug("Showing MainWindow");
		});
	}

	public void showUnlockWindow(Vault vault) {
	public void startUnlockWorkflow(Vault vault) {
		Platform.runLater(() -> {
			Stage stage = unlockWindowBuilder.vault(vault).build().showUnlockWindow();
			addVisibleStage(stage);
			unlockWindowBuilderProvider.get().vault(vault).build().startUnlockWorkflow();
			LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName());
		});
	}

	public void showQuitWindow(QuitResponse response) {
		Platform.runLater(() -> {
			Stage stage = quitWindowBuilder.quitResponse(response).build().showQuitWindow();
			addVisibleStage(stage);
			quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
			LOG.debug("Showing QuitWindow");
		});
	}


@@ 129,15 121,14 @@ public class FxApplication extends Application {
	private void loadSelectedStyleSheet(UiTheme desiredTheme) {
		UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT;
		switch (theme) {
			case DARK:
			case DARK -> {
				Application.setUserAgentStylesheet(getClass().getResource("/css/dark_theme.css").toString());
				macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToDarkAqua));
				break;
			case LIGHT:
			default:
			}
			case LIGHT -> {
				Application.setUserAgentStylesheet(getClass().getResource("/css/light_theme.css").toString());
				macFunctions.map(MacFunctions::uiAppearance).ifPresent(JniException.ignore(MacApplicationUiAppearance::setToAqua));
				break;
			}
		}
	}


M main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java => main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +21 -3
@@ 11,10 11,14 @@ import dagger.Provides;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
import org.cryptomator.ui.preferences.PreferencesComponent;
import org.cryptomator.ui.quit.QuitComponent;


@@ 32,8 36,8 @@ abstract class FxApplicationModule {

	@Provides
	@FxApplicationScoped
	static ObjectProperty<Vault> provideSelectedVault() {
		return new SimpleObjectProperty<>();
	static ObservableSet<Stage> provideVisibleStages() {
		return FXCollections.observableSet();
	}

	@Provides


@@ 43,7 47,6 @@ abstract class FxApplicationModule {
		if (SystemUtils.IS_OS_MAC) {
			return Collections.emptyList();
		}

		try {
			return List.of( //
					createImageFromResource("/window_icon_32.png"), //


@@ 53,6 56,21 @@ abstract class FxApplicationModule {
			throw new UncheckedIOException("Failed to load embedded resource.", e);
		}
	}
	
	@Provides
	@FxApplicationScoped
	static StageFactory provideStageFactory(@Named("windowIcons") List<Image> windowIcons, ObservableSet<Stage> visibleStages) {
		return new StageFactory(stage -> {
			stage.getIcons().addAll(windowIcons);
			stage.showingProperty().addListener((observableValue, wasShowing, isShowing) -> {
				if (isShowing) {
					visibleStages.add(stage);
				} else {
					visibleStages.remove(stage);
				}
			});
		});
	}

	private static Image createImageFromResource(String resourceName) throws IOException {
		try (InputStream in = FxApplicationModule.class.getResourceAsStream(resourceName)) {

M main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java => main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java +3 -9
@@ 52,19 52,13 @@ class AppLaunchEventHandler {

	private void handleLaunchEvent(boolean hasTrayIcon, AppLaunchEvent event) {
		switch (event.getType()) {
			case REVEAL_APP:
				fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
				break;
			case OPEN_FILE:
				fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
			case REVEAL_APP -> fxApplicationStarter.get(hasTrayIcon).thenAccept(FxApplication::showMainWindow);
			case OPEN_FILE -> fxApplicationStarter.get(hasTrayIcon).thenRun(() -> {
					Platform.runLater(() -> {
						event.getPathsToOpen().forEach(this::addVault);
					});
				});
				break;
			default:
				LOG.warn("Unsupported event type: {}", event.getType());
				break;
			default -> LOG.warn("Unsupported event type: {}", event.getType());
		}
	}


M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java => main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java +5 -1
@@ 64,7 64,11 @@ public class UiLauncher {
		// auto unlock
		Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
		if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
			fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
			fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
				for (Vault vault : vaultsWithAutoUnlockEnabled){
					app.startUnlockWorkflow(vault);
				}
			});
		}

		launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +18 -1
@@ 1,12 1,17 @@
package org.cryptomator.ui.mainwindow;

import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.scene.input.DragEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;


@@ 28,15 33,19 @@ public class MainWindowController implements FxController {

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

	private final Stage window;
	private final VaultListManager vaultListManager;
	private final ReadOnlyObjectProperty<Vault> selectedVault;
	private final WrongFileAlertComponent.Builder wrongFileAlert;
	private final BooleanProperty draggingOver = new SimpleBooleanProperty();
	private final BooleanProperty draggingVaultOver = new SimpleBooleanProperty();
	public StackPane root;

	@Inject
	public MainWindowController(VaultListManager vaultListManager, WrongFileAlertComponent.Builder wrongFileAlert) {
	public MainWindowController(@MainWindow Stage window, VaultListManager vaultListManager, ObjectProperty<Vault> selectedVault, WrongFileAlertComponent.Builder wrongFileAlert) {
		this.window = window;
		this.vaultListManager = vaultListManager;
		this.selectedVault = selectedVault;
		this.wrongFileAlert = wrongFileAlert;
	}



@@ 50,6 59,14 @@ public class MainWindowController implements FxController {
		if (SystemUtils.IS_OS_WINDOWS) {
			root.getStyleClass().add("os-windows");
		}
		window.focusedProperty().addListener(this::mainWindowFocusChanged);
	}

	private void mainWindowFocusChanged(Observable observable) {
		var v = selectedVault.get();
		if (v != null) {
			VaultListManager.redetermineVaultState(v);
		}
	}

	private void handleDragEvent(DragEvent event) {

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +14 -4
@@ 4,16 4,21 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;


@@ 29,6 34,12 @@ import java.util.ResourceBundle;
abstract class MainWindowModule {

	@Provides
	@MainWindowScoped
	static ObjectProperty<Vault> provideSelectedVault() {
		return new SimpleObjectProperty<>();
	}

	@Provides
	@MainWindow
	@MainWindowScoped
	static FXMLLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, MainWindowSceneFactory sceneFactory, ResourceBundle resourceBundle) {


@@ 38,22 49,21 @@ abstract class MainWindowModule {
	@Provides
	@MainWindow
	@MainWindowScoped
	static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage(StageStyle.UNDECORATED);
	static Stage provideStage(StageFactory factory) {
		Stage stage = factory.create(StageStyle.UNDECORATED);
		// TODO: min/max values chosen arbitrarily. We might wanna take a look at the user's resolution...
		stage.setMinWidth(650);
		stage.setMinHeight(440);
		stage.setMaxWidth(1000);
		stage.setMaxHeight(700);
		stage.setTitle("Cryptomator");
		stage.getIcons().addAll(windowIcons);
		return stage;
	}

	@Provides
	@FxmlScene(FxmlFile.MAIN_WINDOW)
	@MainWindowScoped
	static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders, MainWindowController mainWindowController, VaultListController vaultListController) {
	static Scene provideMainScene(@MainWindow FXMLLoaderFactory fxmlLoaders) {
		return fxmlLoaders.createScene("/fxml/main_window.fxml");
	}


M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +1 -1
@@ 26,7 26,7 @@ public class VaultDetailLockedController implements FxController {

	@FXML
	public void unlock() {
		application.showUnlockWindow(vault.get());
		application.startUnlockWorkflow(vault.get());
	}

	@FXML

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultListController.java +1 -15
@@ 64,21 64,7 @@ public class VaultListController implements FxController {
		if (newValue == null) {
			return;
		}
		VaultState reportedState = newValue.getState();
		switch (reportedState) {
			case LOCKED, NEEDS_MIGRATION, MISSING:
				try {
					VaultState determinedState = VaultListManager.determineVaultState(newValue.getPath());
					newValue.setState(determinedState);
				} catch (IOException e) {
					LOG.warn("Failed to determine vault state for " + newValue.getPath(), e);
					newValue.setState(VaultState.ERROR);
					newValue.setLastKnownException(e);
				}
				break;
			case ERROR, UNLOCKED, PROCESSING:
				break; // no-op
		}
		VaultListManager.redetermineVaultState(newValue);
	}

	@FXML

M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java => main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java +3 -3
@@ 17,6 17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;

import javax.inject.Named;


@@ 38,13 39,12 @@ abstract class MigrationModule {
	@Provides
	@MigrationWindow
	@MigrationScoped
	static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("migration.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}
	

M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java => main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +1 -1
@@ 121,7 121,7 @@ public class MigrationRunController implements FxController {
			} else {
				LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
				vault.setState(VaultState.LOCKED);
				passwordField.swipe();
				passwordField.wipe();
				window.setScene(successScene.get());
			}
		}).onError(InvalidPassphraseException.class, e -> {

M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java => main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java +1 -4
@@ 1,8 1,5 @@
package org.cryptomator.ui.migration;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;


@@ 28,7 25,7 @@ public class MigrationSuccessController implements FxController {
	@FXML
	public void unlockAndClose() {
		close();
		fxApplication.showUnlockWindow(vault);
		fxApplication.startUnlockWorkflow(vault);
	}

	@FXML

M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java => main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +12 -1
@@ 12,9 12,11 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;
import javafx.application.Application;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.Environment;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@@ 37,6 39,8 @@ public class GeneralPreferencesController implements FxController {
	private final LicenseHolder licenseHolder;
	private final ExecutorService executor;
	private final ResourceBundle resourceBundle;
	private final Application application;
	private final Environment environment;
	public ChoiceBox<UiTheme> themeChoiceBox;
	public CheckBox startHiddenCheckbox;
	public CheckBox debugModeCheckbox;


@@ 46,7 50,7 @@ public class GeneralPreferencesController implements FxController {
	public RadioButton nodeOrientationRtl;

	@Inject
	GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle) {
	GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) {
		this.settings = settings;
		this.trayMenuSupported = trayMenuSupported;
		this.autoStartStrategy = autoStartStrategy;


@@ 54,6 58,8 @@ public class GeneralPreferencesController implements FxController {
		this.licenseHolder = licenseHolder;
		this.executor = executor;
		this.resourceBundle = resourceBundle;
		this.application = application;
		this.environment = environment;
	}

	@FXML


@@ 115,6 121,11 @@ public class GeneralPreferencesController implements FxController {
		selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
	}

	@FXML
	public void showLogfileDirectory(){
		environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
	}

	/* Helper classes */

	private static class UiThemeConverter extends StringConverter<UiTheme> {

M main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java => main/ui/src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java +3 -3
@@ 15,6 15,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;

import javax.inject.Named;
import javax.inject.Provider;


@@ 41,11 42,10 @@ abstract class PreferencesModule {
	@Provides
	@PreferencesWindow
	@PreferencesScoped
	static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("preferences.title"));
		stage.setResizable(false);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java => main/ui/src/main/java/org/cryptomator/ui/quit/QuitModule.java +3 -3
@@ 17,6 17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;

import javax.inject.Named;
import javax.inject.Provider;


@@ 37,12 38,11 @@ abstract class QuitModule {
	@Provides
	@QuitWindow
	@QuitScoped
	static Stage provideStage(@Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory) {
		Stage stage = factory.create();
		stage.setMinWidth(300);
		stage.setMinHeight(100);
		stage.initModality(Modality.APPLICATION_MODAL);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java => main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +3 -3
@@ 21,6 21,7 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
import org.cryptomator.ui.common.StageFactory;

import javax.inject.Named;
import javax.inject.Provider;


@@ 41,13 42,12 @@ abstract class RecoveryKeyModule {
	@Provides
	@RecoveryKeyWindow
	@RecoveryKeyScoped
	static Stage provideStage(ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons, @Named("keyRecoveryOwner") Stage owner) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, ResourceBundle resourceBundle, @Named("keyRecoveryOwner") Stage owner) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("recoveryKey.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}
	

M main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java => main/ui/src/main/java/org/cryptomator/ui/removevault/RemoveVaultModule.java +3 -3
@@ 17,6 17,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;

import javax.inject.Named;


@@ 38,13 39,12 @@ abstract class RemoveVaultModule {
	@Provides
	@RemoveVaultWindow
	@RemoveVaultScoped
	static Stage provideStage(@MainWindow Stage owner,  ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("removeVault.title"));
		stage.setResizable(false);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java => main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +1 -1
@@ 103,7 103,7 @@ class TrayMenuController {
	}

	private void unlockVault(Vault vault) {
		fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
		fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault));
	}

	private void lockVault(Vault vault) {

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java +12 -10
@@ 14,21 14,23 @@ import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.common.vaults.Vault;

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

@UnlockScoped
@Subcomponent(modules = {UnlockModule.class})
public interface UnlockComponent {

	@UnlockWindow
	Stage window();

	@FxmlScene(FxmlFile.UNLOCK)
	Lazy<Scene> scene();
	ExecutorService defaultExecutorService();

	default Stage showUnlockWindow() {
		Stage stage = window();
		stage.setScene(scene().get());
		stage.show();
		return stage;
	UnlockWorkflow unlockWorkflow();
	
	default Future<Boolean> startUnlockWorkflow() {
		UnlockWorkflow workflow = unlockWorkflow();
		defaultExecutorService().submit(workflow);
		return workflow;
	}

	@Subcomponent.Builder

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +51 -109
@@ 1,39 1,30 @@
package org.cryptomator.ui.unlock;

import dagger.Lazy;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.NotDirectoryException;
import javax.inject.Named;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@UnlockScoped
public class UnlockController implements FxController {


@@ 42,124 33,70 @@ public class UnlockController implements FxController {

	private final Stage window;
	private final Vault vault;
	private final ExecutorService executor;
	private final ObjectBinding<ContentDisplay> unlockButtonState;
	private final Optional<KeychainAccess> keychainAccess;
	private final VaultService vaultService;
	private final Lazy<Scene> successScene;
	private final Lazy<Scene> invalidMountPointScene;
	private final ErrorComponent.Builder errorComponent;
	private final AtomicReference<char[]> password;
	private final AtomicBoolean savePassword;
	private final Optional<char[]> savedPassword;
	private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
	private final ForgetPasswordComponent.Builder forgetPassword;
	private final Optional<KeychainAccess> keychainAccess;
	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
	private final BooleanBinding userInteractionDisabled;
	private final BooleanProperty unlockButtonDisabled;
	public NiceSecurePasswordField passwordField;
	public CheckBox savePassword;
	public CheckBox savePasswordCheckbox;

	@Inject
	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, ForgetPasswordComponent.Builder forgetPassword) {
	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainAccess> keychainAccess) {
		this.window = window;
		this.vault = vault;
		this.executor = executor;
		this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
		this.keychainAccess = keychainAccess;
		this.vaultService = vaultService;
		this.successScene = successScene;
		this.invalidMountPointScene = invalidMountPointScene;
		this.errorComponent = errorComponent;
		this.password = password;
		this.savePassword = savePassword;
		this.savedPassword = savedPassword;
		this.passwordEntryLock = passwordEntryLock;
		this.forgetPassword = forgetPassword;
		this.keychainAccess = keychainAccess;
		this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
		this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
		this.unlockButtonDisabled = new SimpleBooleanProperty();
	}

	public void initialize() {
		if (keychainAccess.isPresent()) {
			loadStoredPassword();
		} else {
			savePassword.setSelected(false);
		savePasswordCheckbox.setSelected(savedPassword.isPresent());
		if (password.get() != null) {
			passwordField.setPassword(password.get());
		}
		unlockButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.LOCKED).or(passwordField.textProperty().isEmpty()));
		unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
	}

	@FXML
	public void cancel() {
		LOG.debug("Unlock canceled by user.");
		window.close();
		passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
	}

	@FXML
	public void unlock() {
		LOG.trace("UnlockController.unlock()");
		CharSequence password = passwordField.getCharacters();

		Task<Vault> task = vaultService.createUnlockTask(vault, password);
		passwordField.setDisable(true);
		task.setOnSucceeded(event -> {
			passwordField.setDisable(false);
			if (keychainAccess.isPresent() && savePassword.isSelected()) {
				try {
					keychainAccess.get().storePassphrase(vault.getId(), password);
				} catch (KeychainAccessException e) {
					LOG.error("Failed to store passphrase in system keychain.", e);
				}
			}
			passwordField.swipe();
			LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
			window.setScene(successScene.get());
		});
		task.setOnFailed(event -> {
			passwordField.setDisable(false);
			if (task.getException() instanceof InvalidPassphraseException) {
				Animations.createShakeWindowAnimation(window).play();
				passwordField.selectAll();
				passwordField.requestFocus();
			} else if (task.getException() instanceof NotDirectoryException || task.getException() instanceof DirectoryNotEmptyException) {
				LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
				window.setScene(invalidMountPointScene.get());
			} else {
				LOG.error("Unlock failed for technical reasons.", task.getException());
				errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
			}
		});
		executor.execute(task);
		CharSequence pwFieldContents = passwordField.getCharacters();
		char[] newPw = new char[pwFieldContents.length()];
		for (int i = 0; i < pwFieldContents.length(); i++) {
			newPw[i] = pwFieldContents.charAt(i);
		}
		char[] oldPw = password.getAndSet(newPw);
		if (oldPw != null) {
			Arrays.fill(oldPw, ' ');
		}
		passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
	}

	/* Save Password */

	@FXML
	private void didClickSavePasswordCheckbox() {
		if (!savePassword.isSelected() && hasStoredPassword()) {
			forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePassword.setSelected(!forgotten));
		}
	}

	private void loadStoredPassword() {
		assert keychainAccess.isPresent();
		char[] storedPw = null;
		try {
			storedPw = keychainAccess.get().loadPassphrase(vault.getId());
			if (storedPw != null) {
				savePassword.setSelected(true);
				passwordField.setPassword(storedPw);
				passwordField.selectRange(storedPw.length, storedPw.length);
			}
		} catch (KeychainAccessException e) {
			LOG.error("Failed to load entry from system keychain.", e);
		} finally {
			if (storedPw != null) {
				Arrays.fill(storedPw, ' ');
			}
		}
	}

	private boolean hasStoredPassword() {
		char[] storedPw = null;
		try {
			storedPw = keychainAccess.get().loadPassphrase(vault.getId());
			return storedPw != null;
		} catch (KeychainAccessException e) {
			return false;
		} finally {
			if (storedPw != null) {
				Arrays.fill(storedPw, ' ');
			}
		savePassword.set(savePasswordCheckbox.isSelected());
		if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
			forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
		}
	}



@@ 169,15 106,20 @@ public class UnlockController implements FxController {
		return vault;
	}

	public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
		return unlockButtonState;
	public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
		return unlockButtonContentDisplay;
	}

	public ContentDisplay getUnlockButtonContentDisplay() {
		return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
	}

	public BooleanBinding userInteractionDisabledProperty() {
		return userInteractionDisabled;
	}

	public ContentDisplay getUnlockButtonState() {
		return switch (vault.getState()) {
			case PROCESSING -> ContentDisplay.LEFT;
			default -> ContentDisplay.TEXT_ONLY;
		};
	public boolean isUserInteractionDisabled() {
		return userInteractionDisabled.get();
	}

	public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +1 -1
@@ 33,7 33,7 @@ public class UnlockInvalidMountPointController implements FxController {
	/* Getter/Setter */

	public String getMountPoint() {
		return vault.getVaultSettings().getIndividualMountPath().orElse("AUTO");
		return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
	}

}

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +52 -3
@@ 9,23 9,73 @@ import javafx.scene.image.Image;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Named;
import javax.inject.Provider;
import java.nio.CharBuffer;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Module(subcomponents = {ForgetPasswordComponent.class})
abstract class UnlockModule {

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

	public enum PasswordEntry {PASSWORD_ENTERED, CANCELED}

	@Provides
	@UnlockScoped
	static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
		return new UserInteractionLock<>(null);
	}

	@Provides
	@Named("savedPassword")
	@UnlockScoped
	static Optional<char[]> provideStoredPassword(Optional<KeychainAccess> keychainAccess, @UnlockWindow Vault vault) {
		return keychainAccess.map(k -> {
			try {
				return k.loadPassphrase(vault.getId());
			} catch (KeychainAccessException e) {
				LOG.error("Failed to load entry from system keychain.", e);
				return null;
			}
		});
	}
	
	@Provides
	@UnlockScoped
	static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
		return new AtomicReference(storedPassword.orElse(null));
	}

	@Provides
	@Named("savePassword")
	@UnlockScoped
	static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
		return new AtomicBoolean(storedPassword.isPresent());
	}

	@Provides
	@UnlockWindow
	@UnlockScoped


@@ 36,12 86,11 @@ abstract class UnlockModule {
	@Provides
	@UnlockWindow
	@UnlockScoped
	static Stage provideStage(@UnlockWindow Vault vault, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @UnlockWindow Vault vault) {
		Stage stage = factory.create();
		stage.setTitle(vault.getDisplayableName());
		stage.setResizable(false);
		stage.initModality(Modality.APPLICATION_MODAL);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java +10 -0
@@ 7,8 7,10 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.VaultService;


@@ 29,6 31,8 @@ public class UnlockSuccessController implements FxController {
	private final VaultService vaultService;
	private final ObjectProperty<ContentDisplay> revealButtonState;
	private final BooleanProperty revealButtonDisabled;
	
	public CheckBox rememberChoiceCheckbox;

	@Inject
	public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {


@@ 44,6 48,9 @@ public class UnlockSuccessController implements FxController {
	public void close() {
		LOG.trace("UnlockSuccessController.close()");
		window.close();
		if (rememberChoiceCheckbox.isSelected()) {
			vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.IGNORE);
		}
	}

	@FXML


@@ 64,6 71,9 @@ public class UnlockSuccessController implements FxController {
			revealButtonDisabled.set(false);
		});
		executor.execute(revealTask);
		if (rememberChoiceCheckbox.isSelected()) {
			vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.REVEAL);
		}
	}

	/* Getter/Setter */

A main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +190 -0
@@ 0,0 1,190 @@
package org.cryptomator.ui.unlock;

import dagger.Lazy;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Named;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileSystemException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A multi-step task that consists of background activities as well as user interaction.
 * <p>
 * This class runs the unlock process and controls when to display which UI.
 */
@UnlockScoped
public class UnlockWorkflow extends Task<Boolean> {

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

	private final Stage window;
	private final Vault vault;
	private final VaultService vaultService;
	private final AtomicReference<char[]> password;
	private final AtomicBoolean savePassword;
	private final Optional<char[]> savedPassword;
	private final UserInteractionLock<PasswordEntry> passwordEntryLock;
	private final Optional<KeychainAccess> keychain;
	private final Lazy<Scene> unlockScene;
	private final Lazy<Scene> successScene;
	private final Lazy<Scene> invalidMountPointScene;
	private final ErrorComponent.Builder errorComponent;

	@Inject
	UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, Optional<KeychainAccess> keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
		this.window = window;
		this.vault = vault;
		this.vaultService = vaultService;
		this.password = password;
		this.savePassword = savePassword;
		this.savedPassword = savedPassword;
		this.passwordEntryLock = passwordEntryLock;
		this.keychain = keychain;
		this.unlockScene = unlockScene;
		this.successScene = successScene;
		this.invalidMountPointScene = invalidMountPointScene;
		this.errorComponent = errorComponent;
	}

	@Override
	protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
		try {
			if (attemptUnlock()) {
				handleSuccess();
				return true;
			} else {
				cancel(false); // set Tasks state to cancelled
				return false;
			}
		} catch (NotDirectoryException | DirectoryNotEmptyException e) {
			handleInvalidMountPoint(e);
			throw e; // rethrow to trigger correct exception handling in Task
		} catch (CryptoException | Volume.VolumeException | IOException e) {
			handleGenericError(e);
			throw e; // rethrow to trigger correct exception handling in Task
		} finally {
			wipePassword(password.get());
			wipePassword(savedPassword.orElse(null));
		}
	}
	
	private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
		boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
		while (proceed) {
			try {
				vault.unlock(CharBuffer.wrap(password.get()));
				return true;
			} catch (InvalidPassphraseException e) {
				proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
			}
		}
		return false;
	}

	private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
		Platform.runLater(() -> {
			window.setScene(unlockScene.get());
			window.show();
			if (animateShake) {
				Animations.createShakeWindowAnimation(window).play();
			}
		});
		return passwordEntryLock.awaitInteraction();
	}

	private void handleSuccess() {
		LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
		if (savePassword.get()) {
			savePasswordToSystemkeychain();
		}
		switch (vault.getVaultSettings().actionAfterUnlock().get()) {
			case ASK -> Platform.runLater(() -> {
				window.setScene(successScene.get());
				window.show();
			});
			case REVEAL -> {
				Platform.runLater(window::close);
				vaultService.reveal(vault);
			}
			case IGNORE -> Platform.runLater(window::close);
		}
	}

	private void savePasswordToSystemkeychain() {
		if (keychain.isPresent()) {
			try {
				keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
			} catch (KeychainAccessException e) {
				LOG.error("Failed to store passphrase in system keychain.", e);
			}
		}
	}

	private void handleInvalidMountPoint(FileSystemException e) {
		LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
		Platform.runLater(() -> {
			window.setScene(invalidMountPointScene.get());
		});
	}

	private void handleGenericError(Exception e) {
		LOG.error("Unlock failed for technical reasons.", e);
		Platform.runLater(() -> {
			errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
		});
	}

	private void wipePassword(char[] pw) {
		if (pw != null) {
			Arrays.fill(pw, ' ');
		}
	}

	@Override
	protected void scheduled() {
		vault.setState(VaultState.PROCESSING);
	}

	@Override
	protected void succeeded() {
		vault.setState(VaultState.UNLOCKED);
	}

	@Override
	protected void failed() {
		vault.setState(VaultState.LOCKED);
	}

	@Override
	protected void cancelled() {
		vault.setState(VaultState.LOCKED);
	}

}

M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java => main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +33 -1
@@ 2,24 2,56 @@ package org.cryptomator.ui.vaultoptions;

import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;

import javax.inject.Inject;
import java.util.ResourceBundle;

@VaultOptionsScoped
public class GeneralVaultOptionsController implements FxController {

	private final Vault vault;
	private final ResourceBundle resourceBundle;

	public CheckBox unlockOnStartupCheckbox;
	public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;

	@Inject
	GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
	GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
		this.vault = vault;
		this.resourceBundle = resourceBundle;
	}

	@FXML
	public void initialize() {
		unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
		actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
		actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
		actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle));
	}

	private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
		
		private final ResourceBundle resourceBundle;

		public WhenUnlockedConverter(ResourceBundle resourceBundle) {
			this.resourceBundle = resourceBundle;
		}

		@Override
		public String toString(WhenUnlocked obj) {
			return resourceBundle.getString(obj.getDisplayName());
		}

		@Override
		public WhenUnlocked fromString(String string) {
			throw new UnsupportedOperationException();
		}
	}

}

M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java => main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +7 -7
@@ 85,7 85,7 @@ public class MountOptionsController implements FxController {
		driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
		driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());

		if (vault.getVaultSettings().usesIndividualMountPath().get()) {
		if (vault.getVaultSettings().useCustomMountPath().get()) {
			mountPoint.selectToggle(mountPointCustomDir);
		} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
			mountPoint.selectToggle(mountPointWinDriveLetter);


@@ 93,7 93,7 @@ public class MountOptionsController implements FxController {
			mountPoint.selectToggle(mountPointAuto);
		}

		vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
		vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
		vault.getVaultSettings().winDriveLetter().bind( //
				Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
						.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //


@@ 126,14 126,14 @@ public class MountOptionsController implements FxController {
		}
		File file = directoryChooser.showDialog(window);
		if (file != null) {
			vault.getVaultSettings().individualMountPath().set(file.getAbsolutePath());
			vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
		} else {
			vault.getVaultSettings().individualMountPath().set(null);
			vault.getVaultSettings().customMountPath().set(null);
		}
	}

	private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
		if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().individualMountPath().get())) {
		if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
			chooseCustomMountPoint();
		}
	}


@@ 186,11 186,11 @@ public class MountOptionsController implements FxController {
	}

	public StringProperty customMountPathProperty() {
		return vault.getVaultSettings().individualMountPath();
		return vault.getVaultSettings().customMountPath();
	}

	public String getCustomMountPath() {
		return vault.getVaultSettings().individualMountPath().get();
		return vault.getVaultSettings().customMountPath().get();
	}

}

M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java => main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java +3 -3
@@ 16,6 16,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;



@@ 38,15 39,14 @@ abstract class VaultOptionsModule {
	@Provides
	@VaultOptionsWindow
	@VaultOptionsScoped
	static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @MainWindow Stage owner, @VaultOptionsWindow Vault vault) {
		Stage stage = factory.create();
		stage.setTitle(vault.getDisplayableName());
		stage.setResizable(true);
		stage.setMinWidth(400);
		stage.setMinHeight(300);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.initOwner(owner);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java => main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java +3 -3
@@ 14,6 14,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;

import javax.inject.Named;


@@ 35,13 36,12 @@ abstract class WrongFileAlertModule {
	@Provides
	@WrongFileAlertWindow
	@WrongFileAlertScoped
	static Stage provideStage(@MainWindow Stage mainWindow, ResourceBundle resourceBundle, @Named("windowIcons") List<Image> windowIcons) {
		Stage stage = new Stage();
	static Stage provideStage(StageFactory factory, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
		Stage stage = factory.create();
		stage.setTitle(resourceBundle.getString("wrongFileAlert.title"));
		stage.setResizable(false);
		stage.initOwner(mainWindow);
		stage.initModality(Modality.WINDOW_MODAL);
		stage.getIcons().addAll(windowIcons);
		return stage;
	}


M main/ui/src/main/resources/fxml/preferences_general.fxml => main/ui/src/main/resources/fxml/preferences_general.fxml +4 -1
@@ 34,7 34,10 @@

		<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>

		<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
		<HBox spacing="6" alignment="CENTER_LEFT">
			<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
			<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
		</HBox>

		<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
	</children>

M main/ui/src/main/resources/fxml/unlock.fxml => main/ui/src/main/resources/fxml/unlock.fxml +4 -4
@@ 21,15 21,15 @@
	<children>
		<VBox spacing="6">
			<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
			<NiceSecurePasswordField fx:id="passwordField"/>
			<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.vault.processing}" visible="${controller.keychainAccessAvailable}"/>
			<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
			<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
		</VBox>

		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
				<buttons>
					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.vault.processing}"/>
					<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonState}" disable="${controller.unlockButtonDisabled}">
					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
					<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.unlockButtonDisabled}">
						<graphic>
							<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
						</graphic>

M main/ui/src/main/resources/fxml/unlock_success.fxml => main/ui/src/main/resources/fxml/unlock_success.fxml +5 -1
@@ 10,6 10,7 @@
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns="http://javafx.com/javafx"
	  xmlns:fx="http://javafx.com/fxml"
	  fx:controller="org.cryptomator.ui.unlock.UnlockSuccessController"


@@ 26,7 27,10 @@
				<Circle styleClass="glyph-icon-primary" radius="24"/>
				<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CHECK" glyphSize="24"/>
			</StackPane>
			<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
			<VBox spacing="6">
				<FormattedLabel format="%unlock.success.message" arg1="${controller.vault.displayableName}" wrapText="true" HBox.hgrow="ALWAYS"/>
				<CheckBox text="%unlock.success.rememberChoice" fx:id="rememberChoiceCheckbox"/>
			</VBox>
		</HBox>

		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">

M main/ui/src/main/resources/fxml/vault_options_general.fxml => main/ui/src/main/resources/fxml/vault_options_general.fxml +8 -0
@@ 3,6 3,9 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<VBox xmlns="http://javafx.com/javafx"
	  xmlns:fx="http://javafx.com/fxml"
	  fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"


@@ 12,5 15,10 @@
	</padding>
	<children>
		<CheckBox text="%vaultOptions.general.unlockAfterStartup" fx:id="unlockOnStartupCheckbox"/>

		<HBox spacing="6" alignment="CENTER_LEFT">
			<Label text="%vaultOptions.general.actionAfterUnlock"/>
			<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
		</HBox>
	</children>
</VBox>

M main/ui/src/main/resources/i18n/strings.properties => main/ui/src/main/resources/i18n/strings.properties +6 -0
@@ 96,6 96,7 @@ unlock.savePassword=Save Password
unlock.unlockBtn=Unlock
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.rememberChoice=Remember choice, don't show this again
unlock.success.revealBtn=Reveal Vault
## Invalid Mount Point
unlock.error.invalidMountPoint=Mount point is not an empty directory: %s


@@ 134,6 135,7 @@ preferences.general.theme.dark=Dark
preferences.general.unlockThemes=Unlock dark mode
preferences.general.startHidden=Hide window when starting Cryptomator
preferences.general.debugLogging=Enable debug logging
preferences.general.debugDirectory=Reveal log files
preferences.general.autoStart=Launch Cryptomator on system start
preferences.general.interfaceOrientation=Interface Orientation
preferences.general.interfaceOrientation.ltr=Left to Right


@@ 206,6 208,10 @@ wrongFileAlert.link=For further assistance, visit
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Unlock vault when starting Cryptomator
vaultOptions.general.actionAfterUnlock=After successful unlock
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
vaultOptions.general.actionAfterUnlock.ask=Ask
## Mount
vaultOptions.mount=Mounting
vaultOptions.mount.readonly=Read-Only

M main/ui/src/main/resources/i18n/strings_ar.properties => main/ui/src/main/resources/i18n/strings_ar.properties +7 -0
@@ 95,6 95,7 @@ unlock.savePassword=احفظ كلمة المرور
unlock.unlockBtn=افتح
## Success
unlock.success.message=تم فتح المخزن "%s" بنجاح! يمكنك الوصول إليه الآن.
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
unlock.success.revealBtn=افتح الحافظة
## Invalid Mount Point
unlock.error.invalidMountPoint=نقطة الوصول ليست مجلد فارغ: %s


@@ 128,6 129,8 @@ preferences.title=تفضيلات
## General
preferences.general=عام
preferences.general.theme=الشكل والمظهر
preferences.general.theme.light=فاتح (أبيض)
preferences.general.theme.dark=مظلم (أسود)
preferences.general.unlockThemes=تفعيل الوضع المظلم
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
preferences.general.debugLogging=تمكين سجلات التصحيح


@@ 203,6 206,10 @@ wrongFileAlert.link=لمزيد من المساعدة، قم بزيارة
## General
vaultOptions.general=عام
vaultOptions.general.unlockAfterStartup=فتح قفل المخزن عند بدء تشغيل Cryptomator
vaultOptions.general.actionAfterUnlock=بعد فتح القفل بنجاح
vaultOptions.general.actionAfterUnlock.ignore=‮لا تفعل شيئاً
vaultOptions.general.actionAfterUnlock.reveal=اظهار القرص
vaultOptions.general.actionAfterUnlock.ask=اسأل
## Mount
vaultOptions.mount=القرص الوهمي
vaultOptions.mount.readonly=للقراءة فقط

M main/ui/src/main/resources/i18n/strings_ca.properties => main/ui/src/main/resources/i18n/strings_ca.properties +6 -0
@@ 95,6 95,7 @@ unlock.savePassword=Recorda la contrasenya
unlock.unlockBtn=Desbloqueja
## Success
unlock.success.message="%s" s'ha desbloquejat correctament! Ja es pot accedir a la caixa forta.
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
unlock.success.revealBtn=Mostra la caixa forta
## Invalid Mount Point
unlock.error.invalidMountPoint=El punt de muntatge no és un directori buit: %s


@@ 133,6 134,7 @@ preferences.general.theme.dark=Fosc
preferences.general.unlockThemes=Desbloqueja el tema fosc
preferences.general.startHidden=Amaga la finestra al iniciar Cryptomator
preferences.general.debugLogging=Habilita el registre de depuració
preferences.general.debugDirectory=Mostra els fitxers de registres
preferences.general.autoStart=Executa Cryptomator en engegar el sistema
preferences.general.interfaceOrientation=Orientació de la interfície
preferences.general.interfaceOrientation.ltr=Esquerra a dreta


@@ 205,6 207,10 @@ wrongFileAlert.link=Per rebre assistència, visiteu
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Desbloqueja la caixa forta al iniciar Cryptomator
vaultOptions.general.actionAfterUnlock=Després d'un desbloqueig correcte
vaultOptions.general.actionAfterUnlock.ignore=No facis res
vaultOptions.general.actionAfterUnlock.reveal=Mostra la unitat
vaultOptions.general.actionAfterUnlock.ask=Pregunta
## Mount
vaultOptions.mount=Muntatge
vaultOptions.mount.readonly=Només lectura

M main/ui/src/main/resources/i18n/strings_cs.properties => main/ui/src/main/resources/i18n/strings_cs.properties +12 -0
@@ 95,6 95,7 @@ unlock.savePassword=Uložit heslo
unlock.unlockBtn=Odemknout
## Success
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný.
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
unlock.success.revealBtn=Zobrazit trezor
## Invalid Mount Point
unlock.error.invalidMountPoint=Připojovací bod není prázdný adresář: %s


@@ 107,6 108,7 @@ migration.start.confirm=Ano, můj trezor je plně synchronizován.
## Run
migration.run.enterPassword=Zadejte heslo pro "%s"
migration.run.startMigrationBtn=Migrovat trezor
migration.run.progressHint=Může to chvíli trvat…
## Sucess
migration.success.nextStepsInstructions=Migrace "%s" byla úspěšná.\nNyní můžete svůj trezor odemknout.
migration.success.unlockNow=Odemknout nyní


@@ 118,15 120,21 @@ migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Souborový syst
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Souborový systém neumožňuje čtení.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Souborový systém neumožňuje zápis.
## Impossible
migration.impossible.heading=Trezor není možné zmigrovat
migration.impossible.reason=Trezor nemůže být automaticky zmigrován, protože jeho umístění v úložišti nebo přístupový bod není kompatibilní.
migration.impossible.moreInfo=Trezor může být stále otevřen starší verzí. Pro pokyny, jak ručně migrovat trezor, navštivte

# Preferences
preferences.title=Nastavení
## General
preferences.general=Obecné
preferences.general.theme=Vzhled
preferences.general.theme.light=Světlý
preferences.general.theme.dark=Tmavý
preferences.general.unlockThemes=Odemknout tmavý režim
preferences.general.startHidden=Skrýt okno Cryptomatoru při spuštění
preferences.general.debugLogging=Ladicí režim
preferences.general.debugDirectory=Ukázat logovací soubory
preferences.general.autoStart=Spustit Cryptomator při spuštění systému
preferences.general.interfaceOrientation=Orientace prostředí
preferences.general.interfaceOrientation.ltr=Zleva doprava


@@ 199,6 207,10 @@ wrongFileAlert.link=Pro další informace navštivte
## General
vaultOptions.general=Obecné
vaultOptions.general.unlockAfterStartup=Odemknout trezor při spuštění Cryptomator
vaultOptions.general.actionAfterUnlock=Po úspěšném odemčení
vaultOptions.general.actionAfterUnlock.ignore=Nedělat nic
vaultOptions.general.actionAfterUnlock.reveal=Zobrazit jednotku
vaultOptions.general.actionAfterUnlock.ask=Zeptat se
## Mount
vaultOptions.mount=Připojení
vaultOptions.mount.readonly=Pouze pro čtení

M main/ui/src/main/resources/i18n/strings_de.properties => main/ui/src/main/resources/i18n/strings_de.properties +7 -1
@@ 95,6 95,7 @@ unlock.savePassword=Passwort speichern
unlock.unlockBtn=Entsperren
## Success
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du auf deinen Tresor zugreifen.
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
unlock.success.revealBtn=Tresor anzeigen
## Invalid Mount Point
unlock.error.invalidMountPoint=Einhängepunkt ist kein leeres Verzeichnis: %s


@@ 121,7 122,7 @@ migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Das Dateisyste
## Impossible
migration.impossible.heading=Tresor kann nicht migriert werden
migration.impossible.reason=Der Tresor kann nicht automatisch migriert werden, da sein Speicherort oder Zugangspunkt nicht kompatibel ist.
migration.impossible.moreInfo=Der Tresor kann immer noch mit einer älteren Version geöffnet werden. Eine Anleitung zum manuellen Migrieren eines Tresors findest du unter
migration.impossible.moreInfo=Der Tresor kann auch jetzt noch mit einer älteren Version geöffnet werden. Eine Anleitung zum manuellen Migrieren eines Tresors findest du unter

# Preferences
preferences.title=Einstellungen


@@ 133,6 134,7 @@ preferences.general.theme.dark=Dunkel
preferences.general.unlockThemes=Dunklen Modus freischalten
preferences.general.startHidden=Cryptomator im Hintergrund starten
preferences.general.debugLogging=Diagnoseprotokoll aktivieren
preferences.general.debugDirectory=Protokolldateien anzeigen
preferences.general.autoStart=Cryptomator beim Systemstart starten
preferences.general.interfaceOrientation=Oberflächenausrichtung
preferences.general.interfaceOrientation.ltr=Von links nach rechts


@@ 205,6 207,10 @@ wrongFileAlert.link=Für weitere Unterstützung besuche
## General
vaultOptions.general=Allgemein
vaultOptions.general.unlockAfterStartup=Tresor beim Start von Cryptomator entsperren
vaultOptions.general.actionAfterUnlock=Nach erfolgreichem Entsperren
vaultOptions.general.actionAfterUnlock.ignore=Nichts tun
vaultOptions.general.actionAfterUnlock.reveal=Laufwerk anzeigen
vaultOptions.general.actionAfterUnlock.ask=Nachfragen
## Mount
vaultOptions.mount=Laufwerk
vaultOptions.mount.readonly=Schreibgeschützt

M main/ui/src/main/resources/i18n/strings_es.properties => main/ui/src/main/resources/i18n/strings_es.properties +7 -0
@@ 95,6 95,7 @@ unlock.savePassword=Guardar contraseña
unlock.unlockBtn=Desbloquear
## Success
unlock.success.message=¡"%s" se desbloqueó con éxito! La bóveda ya es accesible.
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
unlock.success.revealBtn=Revelar bóveda
## Invalid Mount Point
unlock.error.invalidMountPoint=El punto de montaje no es un directorio vacío: %s


@@ 128,6 129,8 @@ preferences.title=Preferencias
## General
preferences.general=General
preferences.general.theme=Apariencia
preferences.general.theme.light=Claro
preferences.general.theme.dark=Oscuro
preferences.general.unlockThemes=Desbloquear el modo oscuro
preferences.general.startHidden=Ocultar ventana al iniciar Cryptomator
preferences.general.debugLogging=Habilitar registro de depuración


@@ 203,6 206,10 @@ wrongFileAlert.link=Para más ayuda, visite
## General
vaultOptions.general=General
vaultOptions.general.unlockAfterStartup=Desbloquear bóveda al iniciar Cryptomator
vaultOptions.general.actionAfterUnlock=Después de desbloquear exitosamente
vaultOptions.general.actionAfterUnlock.ignore=No hacer nada
vaultOptions.general.actionAfterUnlock.reveal=Revelar unidad
vaultOptions.general.actionAfterUnlock.ask=Preguntar
## Mount
vaultOptions.mount=Montaje
vaultOptions.mount.readonly=Sólo lectura

M main/ui/src/main/resources/i18n/strings_fr.properties => main/ui/src/main/resources/i18n/strings_fr.properties +10 -4
@@ 95,6 95,7 @@ unlock.savePassword=Enregistrer le mot de passe
unlock.unlockBtn=Déverrouiller
## Success
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible.
unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander
unlock.success.revealBtn=Révéler le coffre
## Invalid Mount Point
unlock.error.invalidMountPoint=Le point de montage n'est pas un répertoire vide : %s


@@ 107,7 108,7 @@ migration.start.confirm=Ce coffre est bien synchronisé
## Run
migration.run.enterPassword=Entrez le mot de passe pour %s
migration.run.startMigrationBtn=Migrer le coffre
migration.run.progressHint=Veuillez patienter, cela peut prendre du temps…
migration.run.progressHint=Ceci peut prendre un certain temps…
## Sucess
migration.success.nextStepsInstructions=“%s” migré.\nVous pouvez à présent déverrouiller ce coffre.
migration.success.unlockNow=Déverouiller


@@ 119,9 120,9 @@ migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Ce système de f
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Le système de fichiers ne permet pas d'être lu.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Le système de fichiers ne permet pas l'écriture.
## Impossible
migration.impossible.heading=Impossible de migrer le coffre
migration.impossible.reason=Le coffre ne peut pas être migré automatiquement car son emplacement de stockage ou son point d'accès n'est pas compatible.
migration.impossible.moreInfo=Le coffre peut toujours être ouvert avec une version plus ancienne. Pour obtenir des instructions sur la façon de migrer manuellement un coffre, visitez
migration.impossible.heading=Impossible de migrer le coffre-fort
migration.impossible.reason=Le coffre-fort ne peut pas être migré automatiquement car son emplacement de stockage ou son point d'accès n'est pas compatible.
migration.impossible.moreInfo=Le coffre-fort peut encore être ouvert avec une version plus ancienne. Pour obtenir des instructions sur la façon de migrer manuellement un coffre-fort, visitez

# Preferences
preferences.title=Préférences


@@ 133,6 134,7 @@ preferences.general.theme.dark=Sombre
preferences.general.unlockThemes=Débloquer le mode nuit
preferences.general.startHidden=Démarrer Cryptomator en mode caché
preferences.general.debugLogging=Activer les logs debug
preferences.general.debugDirectory=Afficher le relevé
preferences.general.autoStart=Lancer Cryptomator au démarrage du système
preferences.general.interfaceOrientation=Orientation de l'interface
preferences.general.interfaceOrientation.ltr=De gauche à droite


@@ 205,6 207,10 @@ wrongFileAlert.link=Pour toute aide supplémentaire, visitez
## General
vaultOptions.general=Général
vaultOptions.general.unlockAfterStartup=Déverrouiller le coffre au démarrage
vaultOptions.general.actionAfterUnlock=Après un déverrouillage réussi
vaultOptions.general.actionAfterUnlock.ignore=Ne rien faire
vaultOptions.general.actionAfterUnlock.reveal=Afficher le disque
vaultOptions.general.actionAfterUnlock.ask=Demander
## Mount
vaultOptions.mount=Montage
vaultOptions.mount.readonly=Lecture seule

M main/ui/src/main/resources/i18n/strings_hi.properties => main/ui/src/main/resources/i18n/strings_hi.properties +1 -1
@@ 92,7 92,7 @@ main.vaultDetail.migrateButton=वाउल्ट को अपग्रेड 

# Wrong File Alert
wrongFileAlert.instruction.1=अपना वाउल्ट खोलें।
wrongFileAlert.link=और मदद लिए, यह जाएं
wrongFileAlert.link=और मदद के लिए, यह जाएं

# Vault Options
## General

M main/ui/src/main/resources/i18n/strings_it.properties => main/ui/src/main/resources/i18n/strings_it.properties +8 -0
@@ 95,6 95,7 @@ unlock.savePassword=Salva password
unlock.unlockBtn=Sblocca
## Success
unlock.success.message=Sbloccato "%s" con successo! La tua cassaforte è ora accessibile.
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
unlock.success.revealBtn=Rivela Cassaforte
## Invalid Mount Point
unlock.error.invalidMountPoint=Il punto di mount non è una cartella vuota: %s


@@ 128,9 129,12 @@ preferences.title=Impostazioni
## General
preferences.general=Generali
preferences.general.theme=Aspetto
preferences.general.theme.light=Chiaro
preferences.general.theme.dark=Scuro
preferences.general.unlockThemes=Sblocca modalità scura
preferences.general.startHidden=Nascondi la finestra all'avvio di Cryptomator
preferences.general.debugLogging=Abilita i registri di debug
preferences.general.debugDirectory=Mostra file log
preferences.general.autoStart=Avvia Cryptomator all'avvio del sistema
preferences.general.interfaceOrientation=Orientamento Interfaccia
preferences.general.interfaceOrientation.ltr=Da Sinistra a Destra


@@ 203,6 207,10 @@ wrongFileAlert.link=Per ulteriore assistenza, visita
## General
vaultOptions.general=Generali
vaultOptions.general.unlockAfterStartup=Sblocca vault all'avvio di Cryptomator
vaultOptions.general.actionAfterUnlock=Dopo aver sbloccato con successo
vaultOptions.general.actionAfterUnlock.ignore=Non fare nulla
vaultOptions.general.actionAfterUnlock.reveal=Visualizza disco
vaultOptions.general.actionAfterUnlock.ask=Chiedi
## Mount
vaultOptions.mount=Montaggio
vaultOptions.mount.readonly=Sola lettura

M main/ui/src/main/resources/i18n/strings_ja.properties => main/ui/src/main/resources/i18n/strings_ja.properties +12 -2
@@ 95,6 95,7 @@ unlock.savePassword=パスワードを保存
unlock.unlockBtn=解錠
## Success
unlock.success.message="%s" の解錠に成功しました! 金庫にアクセス可能です。
unlock.success.rememberChoice=選択を記憶させて、再度表示しない
unlock.success.revealBtn=金庫を表示
## Invalid Mount Point
unlock.error.invalidMountPoint=マウントポイントが空のディレクトリではありません: %s


@@ 107,6 108,7 @@ migration.start.confirm=はい。同期が完了しています
## Run
migration.run.enterPassword="%s" のパスワードを入力してください
migration.run.startMigrationBtn=金庫を移行
migration.run.progressHint=時間がかかる場合があります...
## Sucess
migration.success.nextStepsInstructions="%s" の移行が成功しました。\n金庫を解錠できるようになりました。
migration.success.unlockNow=今すぐ解錠


@@ 118,17 120,21 @@ migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=ファイルシ
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=ファイルシステムによって読み込みが許可されていません。
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=ファイルシステムによって書き込みが許可されていません。
## Impossible
migration.impossible.heading=金庫の移行に失敗しました
migration.impossible.reason=ストレージ場所またはアクセスポイントに互換性がないため、金庫を自動的に移行できません。
migration.impossible.moreInfo=金庫を古いバージョンで開くことは可能です。手動による金庫の移行方法については次をご覧ください:

# Preferences
preferences.title=設定
## General
preferences.general=基本設定
preferences.general.theme=外見 & 操作性
preferences.general.theme.light=明るい
preferences.general.theme.dark=暗い
preferences.general.theme.light=ライト
preferences.general.theme.dark=ダーク
preferences.general.unlockThemes=ダークモードの解錠
preferences.general.startHidden=Cryptomator を開始したときウィンドウを隠す
preferences.general.debugLogging=ログを有効にする
preferences.general.debugDirectory=ログ ファイルを表示
preferences.general.autoStart=システム開始時にCryptomatorを起動する
preferences.general.interfaceOrientation=インターフェイスの向き
preferences.general.interfaceOrientation.ltr=左から右


@@ 201,6 207,10 @@ wrongFileAlert.link=より詳細な手順については、次のページをご
## General
vaultOptions.general=基本設定
vaultOptions.general.unlockAfterStartup=Cryptomatorの起動時に金庫を解錠する
vaultOptions.general.actionAfterUnlock=解錠に成功したあと
vaultOptions.general.actionAfterUnlock.ignore=何もしない
vaultOptions.general.actionAfterUnlock.reveal=ドライブを表示
vaultOptions.general.actionAfterUnlock.ask=尋ねる
## Mount
vaultOptions.mount=マウント
vaultOptions.mount.readonly=読み取り専用

M main/ui/src/main/resources/i18n/strings_ko.properties => main/ui/src/main/resources/i18n/strings_ko.properties +5 -0
@@ 95,6 95,7 @@ unlock.savePassword=비밀번호 저장
unlock.unlockBtn=잠금해제
## Success
unlock.success.message="%s"의 잠금해제가 성공적으로 수행되었습니다! 이제 이 Vault의 접근이 가능합니다.
unlock.success.rememberChoice=선택 기억함, 다시 묻지 않음
unlock.success.revealBtn=Vault 표시
## Invalid Mount Point
unlock.error.invalidMountPoint=구성지점의 디렉터리가 비어있지 않습니다: %s


@@ 205,6 206,10 @@ wrongFileAlert.link=추후 지원을 위해, 다음을 방문하십시요
## General
vaultOptions.general=일반
vaultOptions.general.unlockAfterStartup=Cryptomator를 시작할 때 Vault 잠금 해제
vaultOptions.general.actionAfterUnlock=성공적으로 잠금해제 후
vaultOptions.general.actionAfterUnlock.ignore=아무 것도 하지 않음
vaultOptions.general.actionAfterUnlock.reveal=드라이브 표시
vaultOptions.general.actionAfterUnlock.ask=요청
## Mount
vaultOptions.mount=드라이브 구성
vaultOptions.mount.readonly=읽기 전용

M main/ui/src/main/resources/i18n/strings_lv.properties => main/ui/src/main/resources/i18n/strings_lv.properties +3 -0
@@ 124,6 124,8 @@ preferences.title=Iestatījumi
## General
preferences.general=Vispārēji
preferences.general.theme=Izskats
preferences.general.theme.light=Gaišs
preferences.general.theme.dark=Tumšs
preferences.general.unlockThemes=Iespējot tumšo režīmu
preferences.general.startHidden=Paslēpt logu, kad startē Cryptomator
preferences.general.debugLogging=Iespējot atkļūdošanas žurnalēšanu


@@ 199,6 201,7 @@ wrongFileAlert.link=Lai iegūtu turpmāku palīdzību, apmeklējiet
## General
vaultOptions.general=Vispārēji
vaultOptions.general.unlockAfterStartup=Atslēgt glabātuvi startējot Cryptomator
vaultOptions.general.actionAfterUnlock.reveal=Atklāt disku
## Mount
vaultOptions.mount=Montē
vaultOptions.mount.readonly=Tikai lasīt

M main/ui/src/main/resources/i18n/strings_nb.properties => main/ui/src/main/resources/i18n/strings_nb.properties +11 -0
@@ 95,6 95,7 @@ unlock.savePassword=Lagre passord
unlock.unlockBtn=Lås opp
## Success
unlock.success.message=Vellykket opplåsning av "%s"! Hvelvet ditt er nå tilgjengelig.
unlock.success.rememberChoice=Husk valget - ikke vis dette igjen
unlock.success.revealBtn=Gjør hvelvet synlig
## Invalid Mount Point
unlock.error.invalidMountPoint=Monteringspunktet er ikke en tom mappe: %s


@@ 107,6 108,7 @@ migration.start.confirm=Ja, hvelvet mitt er fullstendig synkronisert
## Run
migration.run.enterPassword=Skriv inn passordet for "%s"
migration.run.startMigrationBtn=Overfør hvelv
migration.run.progressHint=Dette kan ta litt tid…
## Sucess
migration.success.nextStepsInstructions=Vellykket overføring av "%s".\nDu kan nå låse opp hvelvet ditt.
migration.success.unlockNow=Lås opp nå


@@ 118,12 120,17 @@ migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Filsystemet stø
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Filsystemet tillater ikke lesing.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Filsystemet tillater ikke å bli skrevet på.
## Impossible
migration.impossible.heading=Kunne ikke overføre hvelvet
migration.impossible.reason=Hvelvet kan ikke overføres automatisk fordi lagringsstedet eller tilgangspunkt ikke er kompatibelt.
migration.impossible.moreInfo=Hvelvet kan fortsatt åpnes hvis du bruker en eldre versjon. For instruksjoner om hvordan man overfører et hvelv, besøk

# Preferences
preferences.title=Innstillinger
## General
preferences.general=Generelt
preferences.general.theme=Grafisk utseende
preferences.general.theme.light=Lys
preferences.general.theme.dark=Mørk
preferences.general.unlockThemes=Lås opp mørk modus
preferences.general.startHidden=Skjul vinduet når du starter Cryptomator
preferences.general.debugLogging=Aktiver loggføring av feilsøk


@@ 199,6 206,10 @@ wrongFileAlert.link=For ytterligere hjelp, besøk
## General
vaultOptions.general=Generelt
vaultOptions.general.unlockAfterStartup=Lås opp hvelvet når du starter Cryptomator
vaultOptions.general.actionAfterUnlock=Etter vellykket opplåsing
vaultOptions.general.actionAfterUnlock.ignore=Ikke gjør noe
vaultOptions.general.actionAfterUnlock.reveal=Gjør enheten synlig
vaultOptions.general.actionAfterUnlock.ask=Spør
## Mount
vaultOptions.mount=Montering
vaultOptions.mount.readonly=Skrivebeskyttet

M main/ui/src/main/resources/i18n/strings_nl.properties => main/ui/src/main/resources/i18n/strings_nl.properties +1 -0
@@ 197,6 197,7 @@ wrongFileAlert.link=Voor verdere ondersteuning, bezoek
## General
vaultOptions.general=Algemeen
vaultOptions.general.unlockAfterStartup=Ontgrendel kluis bij het starten van Cryptomator
vaultOptions.general.actionAfterUnlock.reveal=Toon Schijf
## Mount
vaultOptions.mount=Aankoppelen
vaultOptions.mount.readonly=Alleen-Lezen

M main/ui/src/main/resources/i18n/strings_nn.properties => main/ui/src/main/resources/i18n/strings_nn.properties +11 -0
@@ 95,6 95,7 @@ unlock.savePassword=Lagre passordet
unlock.unlockBtn=Låse opp
## Success
unlock.success.message=Vellykka opplåsning av "%s"! Kvelven din er no tilgjengeleg.
unlock.success.rememberChoice=Hugs valet - ikkje vis dette igjen
unlock.success.revealBtn=Gjer kvelven synleg
## Invalid Mount Point
unlock.error.invalidMountPoint=Monteringspunktet er ikkje ei tom mappe: %s


@@ 107,6 108,7 @@ migration.start.confirm=Ja, kvelven min er fullstendig synkronisert
## Run
migration.run.enterPassword=Skriv inn passordet for "%s"
migration.run.startMigrationBtn=Migrer kvelv
migration.run.progressHint=Dette kan ta litt tid…
## Sucess
migration.success.nextStepsInstructions=Vellykka migrering av "%s". Du kan no låsa opp kvelven din.
migration.success.unlockNow=Lås opp no


@@ 118,12 120,17 @@ migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Filsystemet stø
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Filsystemet tillèt ikkje å bli lese.
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Filsystemet tillèt ikkje å bli skrive på.
## Impossible
migration.impossible.heading=Kunne ikkje overføra kvelven
migration.impossible.reason=Kvelven kan ikkje overførast automatisk fordi lagringsstaden eller tilgangspunkt ikkje er kompatibelt.
migration.impossible.moreInfo=Kvelven kan framleis opnast viss du bruker ein eldre versjon. For instruksjonar om korleis ein overfører ein kvelv, besøk

# Preferences
preferences.title=Innstillingar
## General
preferences.general=Generelt
preferences.general.theme=Grafisk utsjånad
preferences.general.theme.light=Lys
preferences.general.theme.dark=Mørk
preferences.general.unlockThemes=Lås opp mørk modus
preferences.general.startHidden=Skjul vindauget når du startar Cryptomator
preferences.general.debugLogging=Aktivar protokollføring av feilsøk


@@ 199,6 206,10 @@ wrongFileAlert.link=For ytterlegare hjelp, besøk
## General
vaultOptions.general=Generelt
vaultOptions.general.unlockAfterStartup=Lås opp kvelven når du startar Cryptomator
vaultOptions.general.actionAfterUnlock=Etter vellykka opplåsing
vaultOptions.general.actionAfterUnlock.ignore=Ikkje gjer noko
vaultOptions.general.actionAfterUnlock.reveal=Gjer eininga synleg
vaultOptions.general.actionAfterUnlock.ask=Spør
## Mount
vaultOptions.mount=Montering
vaultOptions.mount.readonly=Skriveverna

M main/ui/src/main/resources/i18n/strings_pt_BR.properties => main/ui/src/main/resources/i18n/strings_pt_BR.properties +5 -0
@@ 95,6 95,7 @@ unlock.savePassword=Salvar Senha
unlock.unlockBtn=Desbloquear
## Success
unlock.success.message="%s" foi desbloqueado com sucesso! Seu cofre agora está acessível.
unlock.success.rememberChoice=Lembrar opção escolhida, não mostrar isto novamente
unlock.success.revealBtn=Revelar Cofre
## Invalid Mount Point
unlock.error.invalidMountPoint=O ponto de montagem não é um diretório vazio: %s


@@ 205,6 206,10 @@ wrongFileAlert.link=Para obter assistência, visite
## General
vaultOptions.general=Geral
vaultOptions.general.unlockAfterStartup=Desbloqueie o cofre ao iniciar o Cryptomator
vaultOptions.general.actionAfterUnlock=Após desbloquear com sucesso
vaultOptions.general.actionAfterUnlock.ignore=Não fazer nada
vaultOptions.general.actionAfterUnlock.reveal=Revelar Volume
vaultOptions.general.actionAfterUnlock.ask=Perguntar
## Mount
vaultOptions.mount=Montagem
vaultOptions.mount.readonly=Somente Leitura

M main/ui/src/main/resources/i18n/strings_ru.properties => main/ui/src/main/resources/i18n/strings_ru.properties +40 -34
@@ 14,12 14,12 @@ generic.button.next=Далее
generic.button.print=Распечатать
## Error
generic.error.title=Произошла непредвиденная ошибка
generic.error.instruction=Этого не должно было произойти. Создайте отчёт об ошибке ниже и опишите, какие шаги к ней привели.
generic.error.instruction=Этого не должно было произойти. Пожалуйста, создайте отчет об ошибке ниже и опишите, какие шаги к ней привели.

# Tray Menu
traymenu.showMainWindow=Показать
traymenu.showPreferencesWindow=Настройки
traymenu.lockAllVaults=Заблокировать всё
traymenu.lockAllVaults=Заблокировать все
traymenu.quitApplication=Выход
traymenu.vault.unlock=Разблокировать
traymenu.vault.lock=Заблокировать


@@ 29,33 29,33 @@ traymenu.vault.reveal=Показать
addvaultwizard.title=Добавить хранилище
## Welcome
addvaultwizard.welcome.newButton=Создать хранилище
addvaultwizard.welcome.existingButton=Открыть имеющееся хранилище
addvaultwizard.welcome.existingButton=Открыть существующее хранилище
## New
### Name
addvaultwizard.new.nameInstruction=Выберите имя для хранилища
addvaultwizard.new.namePrompt=Имя хранилища
addvaultwizard.new.namePrompt=Название хранилища
### Location
addvaultwizard.new.locationInstruction=Где Cryptomator должен хранить зашифрованные файлы вашего хранилища?
addvaultwizard.new.locationLabel=Место хранения
addvaultwizard.new.locationLabel=Расположение хранилища
addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Своё расположение
addvaultwizard.new.directoryPickerButton=Выбрать…
addvaultwizard.new.directoryPickerTitle=Выберите каталог
addvaultwizard.new.fileAlreadyExists=Хранилище не может быть создано по этому пути, потому что некоторые объекты уже существуют.
addvaultwizard.new.invalidName=Неверное имя хранилища. Укажите корректное имя каталога.
addvaultwizard.new.invalidName=Недопустимое название хранилища. Пожалуйста, выберите корректное название каталога.
### Password
addvaultwizard.new.createVaultBtn=Создать хранилище
addvaultwizard.new.generateRecoveryKeyChoice=Вы не сможете получить доступ к своим данным без пароля. Хотите создать ключ для восстановления на случай потери пароля?
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, лучше предостеречься, чем потом жалеть
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да, пожалуй, лучше так, чем потом сожалеть
addvaultwizard.new.generateRecoveryKeyChoice.no=Нет, спасибо, я не потеряю свой пароль
### Information
addvault.new.readme.storageLocation.fileName=ВАЖНО.rtf
addvault.new.readme.storageLocation.1=⚠️  ФАЙЛЫ ХРАНИЛИЩА  ⚠️
addvault.new.readme.storageLocation.2=Это место, где находится ваше хранилище.
addvault.new.readme.storageLocation.2=Это место расположения вашего хранилища.
addvault.new.readme.storageLocation.3=НЕТ
addvault.new.readme.storageLocation.4=• изменяйте любые файлы в этой папке или
addvault.new.readme.storageLocation.5=• добавляйте в эту папку любые файлы для шифрования.
addvault.new.readme.storageLocation.6=Чтобы зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
addvault.new.readme.storageLocation.6=Если вы хотите зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
addvault.new.readme.storageLocation.7=1. Добавьте это хранилище в Cryptomator.
addvault.new.readme.storageLocation.8=2. Разблокируйте хранилище в Cryptomator.
addvault.new.readme.storageLocation.9=3. Откройте место доступа, нажав кнопку "Показать".


@@ 63,19 63,19 @@ addvault.new.readme.storageLocation.10=Если вам нужна помощь, 
addvault.new.readme.accessLocation.fileName=ПРИВЕТСТВИЕ.rtf
addvault.new.readme.accessLocation.1=🔐️  ЗАШИФРОВАННЫЙ ТОМ  🔐️
addvault.new.readme.accessLocation.2=Это место доступа к вашему хранилищу.
addvault.new.readme.accessLocation.3=Любые файлы, добавленные в этот том, будут зашифрованы Cryptomator. Вы можете работать с ним как с любым другим диском/папкой. Здесь отображается только расшифрованное содержимое тома, ваши файлы остаются зашифрованными на жёстком диске постоянно.
addvault.new.readme.accessLocation.3=Любые файлы, добавленные в этот том, будут зашифрованы Cryptomator. Вы можете работать с ним, как с любым другим диском/папкой. Здесь отображается только расшифрованное содержимое тома, ваши файлы остаются зашифрованными на жестком диске постоянно.
addvault.new.readme.accessLocation.4=Этот файл можно удалить.
## Existing
addvaultwizard.existing.instruction=Выберите файл "masterkey.cryptomator" из имеющегося хранилища.
addvaultwizard.existing.instruction=Выберите файл "masterkey.cryptomator" от существующего хранилища.
addvaultwizard.existing.chooseBtn=Выбрать…
addvaultwizard.existing.filePickerTitle=Выберите файл мастер-ключа
addvaultwizard.existing.filePickerTitle=Выберите файл MasterKey
## Success
addvaultwizard.success.nextStepsInstructions=Добавлено хранилище "%s".\nДля добавления данных или доступа к содержимому нужно разблокировать хранилище. Его можно разблокировать и позже.
addvaultwizard.success.unlockNow=Разблокировать

# Remove Vault
removeVault.title=Удалить хранилище
removeVault.information=Cryptomator просто забудет это хранилище. Позже вы можете добавить его снова. Зашифрованные файлы не будут удалены с жёсткого диска.
removeVault.information=Cryptomator просто забудет это хранилище. Вы можете добавить его снова позже. Зашифрованные файлы не будут удалены с жесткого диска.
removeVault.confirmBtn=Удалить хранилище

# Change Password


@@ 95,6 95,7 @@ unlock.savePassword=Сохранить пароль
unlock.unlockBtn=Разблокировать
## Success
unlock.success.message=Разблокировка "%s" успешно выполнена! Доступ в хранилище открыт.
unlock.success.rememberChoice=Запомнить выбор и больше не спрашивать
unlock.success.revealBtn=Показать хранилище
## Invalid Mount Point
unlock.error.invalidMountPoint=Точка монтирования - не пустая папка: %s


@@ 102,18 103,18 @@ unlock.error.invalidMountPoint=Точка монтирования - не пус
# Migration
migration.title=Обновить хранилище
## Start
migration.start.prompt=Хранилище "%s" нужно преобразовать в более новый формат. Прежде чем продолжить, убедитесь, что нет отложенной синхронизации, которая может повлиять на хранилище.
migration.start.prompt=Ваше хранилище "%s" должно быть обновлено до более нового формата. Перед тем, как продолжить, убедитесь в отсутствии отложенной синхронизации, которая может повлиять на хранилище.
migration.start.confirm=Да, моё хранилище полностью синхронизировано
## Run
migration.run.enterPassword=Введите пароль для "%s"
migration.run.startMigrationBtn=Перенести хранилище
migration.run.progressHint=Это может занять некоторое время…
## Sucess
migration.success.nextStepsInstructions=Перенос "%s" успешно выполнен.\nТеперь можно разблокировать хранилище.
migration.success.nextStepsInstructions="%s" перенесено успешно.\nТеперь вы можете разблокировать хранилище.
migration.success.unlockNow=Разблокировать
## Missing file system capabilities
migration.error.missingFileSystemCapabilities.title=Неподдерживаемая файловая система
migration.error.missingFileSystemCapabilities.description=Миграция не была запущена, так как хранилище находится в ненадлежащей файловой системе.
migration.error.missingFileSystemCapabilities.description=Миграция не была запущена, так как ваше хранилище расположено в неподходящей файловой системе.
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Файловая система не поддерживает длинные имена файлов.
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Файловая система не поддерживает длинные пути.
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Файловая система не разрешает чтение.


@@ 127,14 128,15 @@ migration.impossible.moreInfo=Хранилище по-прежнему можн
preferences.title=Настройки
## General
preferences.general=Общие
preferences.general.theme=Оформление
preferences.general.theme=Внешний вид
preferences.general.theme.light=Светлая
preferences.general.theme.dark=Темная
preferences.general.unlockThemes=Разблокировать тёмный режим
preferences.general.theme.dark=Тёмная
preferences.general.unlockThemes=Разблокировать темный режим
preferences.general.startHidden=Скрывать окно при запуске Cryptomator
preferences.general.debugLogging=Вести журнал отладки
preferences.general.debugLogging=Включить ведение журнала отладки
preferences.general.debugDirectory=Показать файлы журнала
preferences.general.autoStart=Запускать Cryptomator при старте системы
preferences.general.interfaceOrientation=Интерфейс
preferences.general.interfaceOrientation=Ориентация интерфейса
preferences.general.interfaceOrientation.ltr=Слева направо
preferences.general.interfaceOrientation.rtl=Справа налево
## Volume


@@ 146,7 148,7 @@ preferences.volume.webdav.scheme=Схема WebDAV
preferences.updates=Обновления
preferences.updates.currentVersion=Текущая версия: %s
preferences.updates.autoUpdateCheck=Автоматически проверять наличие обновлений
preferences.updates.checkNowBtn=Проверить
preferences.updates.checkNowBtn=Проверить сейчас
preferences.updates.updateAvailable=Доступно обновление до версии %s.
## Donation Key
preferences.donationKey=Пожертвование


@@ 165,19 167,19 @@ main.donationKeyMissing.tooltip=Мы будем рады финансовой п
main.dropZone.dropVault=Добавить это хранилище
main.dropZone.unknownDragboardContent=Если вы хотите добавить хранилище, перетащите его в это окно
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь, чтобы добавить хранилище
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь для добавления хранилища
main.vaultlist.contextMenu.remove=Удалить хранилище
main.vaultlist.addVaultBtn=Добавить хранилище
## Vault Detail
### Welcome