mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-20 09:36:55 -04:00
(Auto)Unlock via VaultService (fixes #1044)
This commit is contained in:
@@ -8,11 +8,11 @@ public class StackTraceController implements FxController {
|
||||
|
||||
private final String stackTrace;
|
||||
|
||||
public StackTraceController(Exception cause) {
|
||||
public StackTraceController(Throwable cause) {
|
||||
this.stackTrace = provideStackTrace(cause);
|
||||
}
|
||||
|
||||
static String provideStackTrace(Exception cause) {
|
||||
static String provideStackTrace(Throwable cause) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
cause.printStackTrace(new PrintStream(baos));
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
|
||||
@@ -4,15 +4,20 @@ import javafx.concurrent.Task;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.ui.fxapp.FxApplicationScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -23,10 +28,12 @@ public class VaultService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);
|
||||
|
||||
private final ExecutorService executorService;
|
||||
private final Optional<KeychainAccess> keychain;
|
||||
|
||||
@Inject
|
||||
public VaultService(ExecutorService executorService) {
|
||||
public VaultService(ExecutorService executorService, Optional<KeychainAccess> keychain) {
|
||||
this.executorService = executorService;
|
||||
this.keychain = keychain;
|
||||
}
|
||||
|
||||
public void reveal(Vault vault) {
|
||||
@@ -45,6 +52,72 @@ public class VaultService {
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
for (Vault vault : vaults) {
|
||||
attemptAutoUnlock(vault, keychain.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks a vault in a background thread using a stored passphrase
|
||||
*
|
||||
* @param vault The vault to unlock
|
||||
* @param keychainAccess The system keychain holding the passphrase for the vault
|
||||
*/
|
||||
public void attemptAutoUnlock(Vault vault, KeychainAccess keychainAccess) {
|
||||
executorService.execute(createAutoUnlockTask(vault, keychainAccess));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -60,6 +133,7 @@ public class VaultService {
|
||||
*
|
||||
* @param vault The vault to lock
|
||||
* @param forced Whether to attempt a forced lock
|
||||
* @return The task
|
||||
*/
|
||||
public Task<Vault> createLockTask(Vault vault, boolean forced) {
|
||||
Task<Vault> task = new LockVaultTask(vault, forced);
|
||||
@@ -145,6 +219,93 @@ public class VaultService {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -186,5 +347,4 @@ public class VaultService {
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.cryptomator.ui.launcher;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.keychain.KeychainAccess;
|
||||
import org.cryptomator.keychain.KeychainAccessException;
|
||||
import org.cryptomator.keychain.KeychainModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
class AutoUnlocker {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AutoUnlocker.class);
|
||||
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final Optional<KeychainAccess> keychain;
|
||||
|
||||
@Inject
|
||||
AutoUnlocker(ObservableList<Vault> vaults, Optional<KeychainAccess> keychain) {
|
||||
this.vaults = vaults;
|
||||
this.keychain = keychain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to unlock all vaults that have been configured for auto unlock.
|
||||
* If an attempt fails (i.e. because the stored password is wrong) it will be silently skipped.
|
||||
*/
|
||||
public void autoUnlock() {
|
||||
if (!keychain.isPresent()) {
|
||||
LOG.info("No system keychain found. Skipping auto unlock.");
|
||||
return;
|
||||
}
|
||||
// TODO: do async
|
||||
vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get()).forEach(this::autoUnlock);
|
||||
}
|
||||
|
||||
private void autoUnlock(Vault vault) {
|
||||
if (vault.getState() != VaultState.LOCKED) {
|
||||
LOG.warn("Can't unlock vault {} due to its state {}", vault.getDisplayablePath(), vault.getState());
|
||||
return;
|
||||
}
|
||||
assert keychain.isPresent();
|
||||
char[] storedPw = null;
|
||||
try {
|
||||
storedPw = keychain.get().loadPassphrase(vault.getId());
|
||||
if (storedPw == null) {
|
||||
LOG.warn("No passphrase stored in keychain for vault registered for auto unlocking: {}", vault.getPath());
|
||||
} else {
|
||||
vault.unlock(CharBuffer.wrap(storedPw));
|
||||
// TODO
|
||||
// Platform.runLater(() -> vault.setState(VaultState.UNLOCKED));
|
||||
LOG.info("Unlocked vault {}", vault.getDisplayablePath());
|
||||
}
|
||||
} catch (IOException | Volume.VolumeException | KeychainAccessException e) {
|
||||
LOG.error("Auto unlock failed.", e);
|
||||
} finally {
|
||||
if (storedPw != null) {
|
||||
Arrays.fill(storedPw, ' ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package org.cryptomator.ui.launcher;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.jni.JniException;
|
||||
import org.cryptomator.jni.MacApplicationUiState;
|
||||
import org.cryptomator.jni.MacFunctions;
|
||||
@@ -13,9 +15,8 @@ import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.desktop.AppReopenedEvent;
|
||||
import java.awt.desktop.AppReopenedListener;
|
||||
import java.awt.desktop.SystemEventListener;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
@Singleton
|
||||
@@ -24,19 +25,19 @@ public class UiLauncher {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UiLauncher.class);
|
||||
|
||||
private final Settings settings;
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final TrayMenuComponent.Builder trayComponent;
|
||||
private final FxApplicationStarter fxApplicationStarter;
|
||||
private final AppLaunchEventHandler launchEventHandler;
|
||||
private final AutoUnlocker autoUnlocker;
|
||||
private final Optional<MacFunctions> macFunctions;
|
||||
|
||||
@Inject
|
||||
public UiLauncher(Settings settings, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, AutoUnlocker autoUnlocker, Optional<MacFunctions> macFunctions) {
|
||||
public UiLauncher(Settings settings, ObservableList<Vault> vaults, TrayMenuComponent.Builder trayComponent, FxApplicationStarter fxApplicationStarter, AppLaunchEventHandler launchEventHandler, Optional<MacFunctions> macFunctions) {
|
||||
this.settings = settings;
|
||||
this.vaults = vaults;
|
||||
this.trayComponent = trayComponent;
|
||||
this.fxApplicationStarter = fxApplicationStarter;
|
||||
this.launchEventHandler = launchEventHandler;
|
||||
this.autoUnlocker = autoUnlocker;
|
||||
this.macFunctions = macFunctions;
|
||||
}
|
||||
|
||||
@@ -59,9 +60,12 @@ public class UiLauncher {
|
||||
|
||||
// register app reopen listener
|
||||
Desktop.getDesktop().addAppEventListener((AppReopenedListener) e -> showMainWindowAsync(hasTrayIcon));
|
||||
|
||||
// auto unlock - no shit!
|
||||
autoUnlocker.autoUnlock();
|
||||
|
||||
// auto unlock
|
||||
Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
|
||||
if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
|
||||
fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
|
||||
}
|
||||
|
||||
launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.CheckBox;
|
||||
@@ -27,6 +28,7 @@ import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.Tasks;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -50,22 +52,24 @@ public class UnlockController implements FxController {
|
||||
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 Lazy<Scene> genericErrorScene;
|
||||
private final ObjectProperty<Exception> genericErrorCause;
|
||||
private final ObjectProperty<Throwable> genericErrorCause;
|
||||
private final ForgetPasswordComponent.Builder forgetPassword;
|
||||
private final BooleanProperty unlockButtonDisabled;
|
||||
public NiceSecurePasswordField passwordField;
|
||||
public CheckBox savePassword;
|
||||
|
||||
@Inject
|
||||
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Exception> genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
|
||||
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, @FxmlScene(FxmlFile.UNLOCK_GENERIC_ERROR) Lazy<Scene> genericErrorScene, @Named("genericErrorCause") ObjectProperty<Throwable> genericErrorCause, ForgetPasswordComponent.Builder forgetPassword) {
|
||||
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.genericErrorScene = genericErrorScene;
|
||||
@@ -93,36 +97,36 @@ public class UnlockController implements FxController {
|
||||
public void unlock() {
|
||||
LOG.trace("UnlockController.unlock()");
|
||||
CharSequence password = passwordField.getCharacters();
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
Tasks.create(() -> {
|
||||
vault.unlock(password);
|
||||
|
||||
Task<Vault> task = vaultService.createUnlockTask(vault, password);
|
||||
task.setOnSucceeded(event -> {
|
||||
if (keychainAccess.isPresent() && savePassword.isSelected()) {
|
||||
keychainAccess.get().storePassphrase(vault.getId(), password);
|
||||
try {
|
||||
keychainAccess.get().storePassphrase(vault.getId(), password);
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||
}
|
||||
}
|
||||
}).onSuccess(() -> {
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
passwordField.swipe();
|
||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
|
||||
window.setScene(successScene.get());
|
||||
}).onError(InvalidPassphraseException.class, e -> {
|
||||
shakeWindow();
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
}).onError(NotDirectoryException.class, e -> {
|
||||
LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage());
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
}).onError(DirectoryNotEmptyException.class, e -> {
|
||||
LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage());
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
}).onError(Exception.class, e -> { // including RuntimeExceptions
|
||||
LOG.error("Unlock failed for technical reasons.", e);
|
||||
genericErrorCause.set(e);
|
||||
window.setScene(genericErrorScene.get());
|
||||
}).andFinally(() -> {
|
||||
if (!vault.isUnlocked()) {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
});
|
||||
task.setOnFailed(event -> {
|
||||
if (task.getException() instanceof InvalidPassphraseException) {
|
||||
shakeWindow();
|
||||
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());
|
||||
genericErrorCause.set(task.getException());
|
||||
window.setScene(genericErrorScene.get());
|
||||
}
|
||||
}).runOnce(executor);
|
||||
});
|
||||
executor.execute(task);
|
||||
}
|
||||
|
||||
/* Save Password */
|
||||
|
||||
@@ -50,7 +50,7 @@ abstract class UnlockModule {
|
||||
@Provides
|
||||
@Named("genericErrorCause")
|
||||
@UnlockScoped
|
||||
static ObjectProperty<Exception> provideGenericErrorCause() {
|
||||
static ObjectProperty<Throwable> provideGenericErrorCause() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ abstract class UnlockModule {
|
||||
@Provides
|
||||
@IntoMap
|
||||
@FxControllerKey(StackTraceController.class)
|
||||
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Exception> errorCause) {
|
||||
static FxController provideStackTraceController(@Named("genericErrorCause") ObjectProperty<Throwable> errorCause) {
|
||||
return new StackTraceController(errorCause.get());
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@
|
||||
<Button text="%vaultOptions.general.changePasswordBtn" onAction="#changePassword"/>
|
||||
<Button text="%vaultOptions.general.showRecoveryKeyBtn" onAction="#showRecoveryKey"/>
|
||||
<Button text="TODO recoverVault" onAction="#showRecoverVaultDialogue"/>
|
||||
<CheckBox text="TODO unlock after startup" fx:id="unlockOnStartupCheckbox"/>
|
||||
<CheckBox text="%vaultOptions.general.unlockAfterStartup" fx:id="unlockOnStartupCheckbox"/>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
@@ -208,3 +208,4 @@ passwordStrength.messageLabel.4=Very strong
|
||||
# Quit
|
||||
quit.prompt=Quit application? There are unlocked vaults.
|
||||
quit.lockAndQuit=Lock and Quit
|
||||
vaultOptions.general.unlockAfterStartup=Unlock vault when starting Cryptomator
|
||||
|
||||
Reference in New Issue
Block a user