Browse Source

(Auto)Unlock via VaultService (fixes #1044)

Sebastian Stenzel 5 năm trước cách đây
mục cha
commit
f62faa72ce

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/common/StackTraceController.java

@@ -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);

+ 162 - 2
main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java

@@ -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 {
 	}
 
 
-
 }

+ 0 - 74
main/ui/src/main/java/org/cryptomator/ui/launcher/AutoUnlocker.java

@@ -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, ' ');
-			}
-		}
-	}
-
-}

+ 12 - 8
main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java

@@ -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);
 	}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 30 - 26
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java


+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java

@@ -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());
 	}
 

+ 1 - 1
main/ui/src/main/resources/fxml/vault_options_general.fxml

@@ -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>

+ 1 - 0
main/ui/src/main/resources/i18n/strings.properties

@@ -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