浏览代码

Merge pull request #1603 from cryptomator/feature/autoLock

fixes #274
Sebastian Stenzel 3 年之前
父节点
当前提交
95b98b8a36

+ 2 - 7
main/commons/src/main/java/org/cryptomator/common/CommonsModule.java

@@ -15,6 +15,7 @@ import org.cryptomator.common.settings.SettingsProvider;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultComponent;
 import org.cryptomator.common.vaults.VaultListManager;
+import org.cryptomator.common.vaults.VaultListModule;
 import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.cryptomator.frontend.webdav.WebDavServer;
 import org.slf4j.Logger;
@@ -37,7 +38,7 @@ import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
-@Module(subcomponents = {VaultComponent.class}, includes = {KeychainModule.class})
+@Module(subcomponents = {VaultComponent.class}, includes = {VaultListModule.class, KeychainModule.class})
 public abstract class CommonsModule {
 
 	private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
@@ -87,12 +88,6 @@ public abstract class CommonsModule {
 		return settingsProvider.get();
 	}
 
-	@Provides
-	@Singleton
-	static ObservableList<Vault> provideVaultList(VaultListManager vaultListManager) {
-		return vaultListManager.getVaultList();
-	}
-
 	@Provides
 	@Singleton
 	static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) {

+ 13 - 2
main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -37,6 +37,8 @@ public class VaultSettings {
 	public static final String DEFAULT_MOUNT_FLAGS = "";
 	public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
 	public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
+	public static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
+	public static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
 
 	private static final Random RNG = new Random();
 
@@ -52,7 +54,8 @@ public class VaultSettings {
 	private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
 	private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
 	private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
-
+	private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
+	private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
 	private final StringBinding mountName;
 
 	public VaultSettings(String id) {
@@ -61,7 +64,7 @@ public class VaultSettings {
 	}
 
 	Observable[] observables() {
-		return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock};
+		return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock, autoLockWhenIdle, autoLockIdleSeconds};
 	}
 
 	public static VaultSettings withRandomId() {
@@ -162,6 +165,14 @@ public class VaultSettings {
 		return actionAfterUnlock.get();
 	}
 
+	public BooleanProperty autoLockWhenIdle() {
+		return autoLockWhenIdle;
+	}
+
+	public IntegerProperty autoLockIdleSeconds() {
+		return autoLockIdleSeconds;
+	}
+
 	/* Hashcode/Equals */
 
 	@Override

+ 8 - 0
main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java

@@ -31,6 +31,8 @@ class VaultSettingsJsonAdapter {
 		out.name("mountFlags").value(value.mountFlags().get());
 		out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
 		out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
+		out.name("autoLockWhenIdle").value(value.autoLockWhenIdle().get());
+		out.name("autoLockIdleSeconds").value(value.autoLockIdleSeconds().get());
 		out.endObject();
 	}
 
@@ -48,6 +50,8 @@ class VaultSettingsJsonAdapter {
 		String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
 		int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
 		WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
+		boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
+		int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
 
 		in.beginObject();
 		while (in.hasNext()) {
@@ -66,6 +70,8 @@ class VaultSettingsJsonAdapter {
 				case "mountFlags" -> mountFlags = in.nextString();
 				case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
 				case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
+				case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
+				case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
 				default -> {
 					LOG.warn("Unsupported vault setting found in JSON: " + name);
 					in.skipValue();
@@ -90,6 +96,8 @@ class VaultSettingsJsonAdapter {
 		vaultSettings.mountFlags().set(mountFlags);
 		vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
 		vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
+		vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
+		vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
 		return vaultSettings;
 	}
 

+ 60 - 0
main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java

@@ -0,0 +1,60 @@
+package org.cryptomator.common.vaults;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javafx.collections.ObservableList;
+import java.time.Instant;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+public class AutoLocker {
+
+	private static final Logger LOG = LoggerFactory.getLogger(AutoLocker.class);
+
+	private final ScheduledExecutorService scheduler;
+	private final ObservableList<Vault> vaultList;
+
+	@Inject
+	public AutoLocker(ScheduledExecutorService scheduler, ObservableList<Vault> vaultList) {
+		this.scheduler = scheduler;
+		this.vaultList = vaultList;
+	}
+
+	public void init() {
+		scheduler.scheduleAtFixedRate(this::tick, 0, 1, TimeUnit.MINUTES);
+	}
+
+	private void tick() {
+		vaultList.stream() // all vaults
+				.filter(Vault::isUnlocked) // unlocked vaults
+				.filter(this::exceedsIdleTime) // idle vaults
+				.forEach(this::autolock);
+	}
+
+	private void autolock(Vault vault) {
+		try {
+			vault.lock(false);
+			LOG.info("Autolocked {} after idle timeout", vault.getDisplayName());
+		} catch (Volume.VolumeException | LockNotCompletedException e) {
+			LOG.error("Autolocking failed.", e);
+		}
+	}
+
+	private boolean exceedsIdleTime(Vault vault) {
+		assert vault.isUnlocked();
+		// TODO: shouldn't we read these properties from within FX Application Thread?
+		if (vault.getVaultSettings().autoLockWhenIdle().get()) {
+			int maxIdleSeconds = vault.getVaultSettings().autoLockIdleSeconds().get();
+			var deadline = vault.getStats().getLastActivity().plusSeconds(maxIdleSeconds);
+			return deadline.isBefore(Instant.now());
+		} else {
+			return false;
+		}
+	}
+
+
+}

+ 5 - 6
main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -37,22 +37,21 @@ public class VaultListManager {
 
 	private static final Logger LOG = LoggerFactory.getLogger(VaultListManager.class);
 
+	private final AutoLocker autoLocker;
 	private final VaultComponent.Builder vaultComponentBuilder;
 	private final ObservableList<Vault> vaultList;
 	private final String defaultVaultName;
 
 	@Inject
-	public VaultListManager(VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) {
+	public VaultListManager(ObservableList<Vault> vaultList, AutoLocker autoLocker, VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) {
+		this.vaultList = vaultList;
+		this.autoLocker = autoLocker;
 		this.vaultComponentBuilder = vaultComponentBuilder;
 		this.defaultVaultName = resourceBundle.getString("defaults.vault.vaultName");
-		this.vaultList = FXCollections.observableArrayList(Vault::observables);
 
 		addAll(settings.getDirectories());
 		vaultList.addListener(new VaultListChangeListener(settings.getDirectories()));
-	}
-
-	public ObservableList<Vault> getVaultList() {
-		return vaultList;
+		autoLocker.init();
 	}
 
 	public Vault add(Path pathToVault) throws IOException {

+ 19 - 0
main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java

@@ -0,0 +1,19 @@
+package org.cryptomator.common.vaults;
+
+import dagger.Module;
+import dagger.Provides;
+
+import javax.inject.Singleton;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+@Module
+public class VaultListModule {
+
+	@Provides
+	@Singleton
+	public ObservableList<Vault> provideVaultList() {
+		return FXCollections.observableArrayList(Vault::observables);
+	}
+
+}

+ 17 - 0
main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java

@@ -13,9 +13,11 @@ import javafx.beans.property.LongProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleDoubleProperty;
 import javafx.beans.property.SimpleLongProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.concurrent.ScheduledService;
 import javafx.concurrent.Task;
 import javafx.util.Duration;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
@@ -39,6 +41,7 @@ public class VaultStats {
 	private final LongProperty totalBytesDecrypted = new SimpleLongProperty();
 	private final LongProperty filesRead = new SimpleLongProperty();
 	private final LongProperty filesWritten = new SimpleLongProperty();
+	private final ObjectProperty<Instant> lastActivity = new SimpleObjectProperty<>();
 
 	@Inject
 	VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
@@ -73,9 +76,15 @@ public class VaultStats {
 		toalBytesWritten.set(stats.map(CryptoFileSystemStats::pollTotalBytesWritten).orElse(0L));
 		totalBytesEncrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesEncrypted).orElse(0L));
 		totalBytesDecrypted.set(stats.map(CryptoFileSystemStats::pollTotalBytesDecrypted).orElse(0L));
+		var oldAccessCount = filesRead.get() + filesWritten.get();
 		filesRead.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesRead).orElse(0L));
 		filesWritten.set(stats.map(CryptoFileSystemStats::pollAmountOfAccessesWritten).orElse(0L));
+		var newAccessCount = filesRead.get() + filesWritten.get();
 
+		// check for any I/O activity
+		if (newAccessCount > oldAccessCount) {
+			lastActivity.set(Instant.now());
+		}
 	}
 
 	private double getCacheHitRate(CryptoFileSystemStats stats) {
@@ -175,4 +184,12 @@ public class VaultStats {
 	public LongProperty filesWritten() {return filesWritten;}
 
 	public long getFilesWritten() {return filesWritten.get();}
+
+	public ObjectProperty<Instant> lastActivityProperty() {
+		return lastActivity;
+	}
+
+	public Instant getLastActivity() {
+		return lastActivity.get();
+	}
 }

+ 1 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java

@@ -39,6 +39,7 @@ public enum FontAwesome5Icon {
 	REDO("\uF01E"), //
 	SEARCH("\uF002"), //
 	SPINNER("\uF110"), //
+	STOPWATCH("\uF2F2"), //
 	SYNC("\uF021"), //
 	TIMES("\uF00D"), //
 	TRASH("\uF1F8"), //

+ 11 - 8
main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -1,10 +1,19 @@
 package org.cryptomator.ui.fxapp;
 
 import dagger.Lazy;
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.ObservableList;
+import javafx.stage.Stage;
+import javafx.stage.Window;
 import org.cryptomator.common.LicenseHolder;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.UiTheme;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.common.vaults.VaultState;
 import org.cryptomator.integrations.tray.TrayIntegrationProvider;
 import org.cryptomator.integrations.uiappearance.Theme;
@@ -24,18 +33,12 @@ import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
-import javafx.application.Application;
-import javafx.application.Platform;
-import javafx.beans.binding.Bindings;
-import javafx.beans.binding.BooleanBinding;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.ObservableList;
-import javafx.stage.Stage;
-import javafx.stage.Window;
 import java.awt.desktop.QuitResponse;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 @FxApplicationScoped
 public class FxApplication extends Application {

+ 51 - 0
main/ui/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java

@@ -0,0 +1,51 @@
+package org.cryptomator.ui.vaultoptions;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.controls.NumericTextField;
+
+import javax.inject.Inject;
+import javafx.beans.binding.Bindings;
+import javafx.fxml.FXML;
+import javafx.scene.control.CheckBox;
+import javafx.util.StringConverter;
+
+@VaultOptionsScoped
+public class AutoLockVaultOptionsController implements FxController {
+
+	private final Vault vault;
+
+	public CheckBox lockAfterTimeCheckbox;
+	public NumericTextField lockTimeInMinutesTextField;
+
+	@Inject
+	AutoLockVaultOptionsController(@VaultOptionsWindow Vault vault) {
+		this.vault = vault;
+	}
+
+	@FXML
+	public void initialize() {
+		lockAfterTimeCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().autoLockWhenIdle());
+		Bindings.bindBidirectional(lockTimeInMinutesTextField.textProperty(), vault.getVaultSettings().autoLockIdleSeconds(), new IdleTimeSecondsConverter());
+	}
+
+	private static class IdleTimeSecondsConverter extends StringConverter<Number> {
+
+		@Override
+		public String toString(Number seconds) {
+			int minutes = seconds.intValue() / 60; // int-truncate
+			return Integer.toString(minutes);
+		}
+
+		@Override
+		public Number fromString(String string) {
+			try {
+				int minutes = Integer.valueOf(string);
+				return minutes * 60;
+			} catch (NumberFormatException e) {
+				return 0;
+			}
+		}
+	}
+
+}

+ 6 - 0
main/ui/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java

@@ -20,4 +20,10 @@ public enum SelectedVaultOptionsTab {
 	 * Show password tab
 	 */
 	KEY,
+
+	/**
+	 * Show Auto-Lock tab
+	 *
+	 */
+	AUTOLOCK,
 }

+ 2 - 0
main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java

@@ -23,6 +23,7 @@ public class VaultOptionsController implements FxController {
 	public Tab generalTab;
 	public Tab mountTab;
 	public Tab keyTab;
+	public Tab autoLockTab;
 
 	@Inject
 	VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
@@ -47,6 +48,7 @@ public class VaultOptionsController implements FxController {
 			case ANY, GENERAL -> generalTab;
 			case MOUNT -> mountTab;
 			case KEY -> keyTab;
+			case AUTOLOCK ->  autoLockTab;
 		};
 	}
 

+ 5 - 0
main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java

@@ -84,4 +84,9 @@ abstract class VaultOptionsModule {
 	@FxControllerKey(MasterkeyOptionsController.class)
 	abstract FxController bindMasterkeyOptionsController(MasterkeyOptionsController controller);
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(AutoLockVaultOptionsController.class)
+	abstract FxController bindAutoLockVaultOptionsController(AutoLockVaultOptionsController controller);
+
 }

+ 8 - 0
main/ui/src/main/resources/fxml/vault_options.fxml

@@ -36,5 +36,13 @@
 				<fx:include source="/fxml/vault_options_masterkey.fxml"/>
 			</content>
 		</Tab>
+		<Tab fx:id="autoLockTab" id="AUTOLOCK" text="%vaultOptions.autoLock">
+			<graphic>
+				<FontAwesome5IconView glyph="STOPWATCH"/>
+			</graphic>
+			<content>
+				<fx:include source="/fxml/vault_options_autolock.fxml"/>
+			</content>
+		</Tab>
 	</tabs>
 </TabPane>

+ 26 - 0
main/ui/src/main/resources/fxml/vault_options_autolock.fxml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FormattedLabel?>
+<?import org.cryptomator.ui.controls.NumericTextField?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Text?>
+<?import javafx.scene.text.TextFlow?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.vaultoptions.AutoLockVaultOptionsController"
+	  spacing="6">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<children>
+		<TextFlow styleClass="text-flow" prefWidth="-Infinity">
+			<CheckBox text="%vaultOptions.autoLock.lockAfterTimePart1" fx:id="lockAfterTimeCheckbox"/>
+			<Text text=" "/>
+			<NumericTextField fx:id="lockTimeInMinutesTextField" prefWidth="50"/>
+			<Text text=" "/>
+			<FormattedLabel format="%vaultOptions.autoLock.lockAfterTimePart2"/>
+		</TextFlow>
+	</children>
+</VBox>

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

@@ -329,6 +329,10 @@ vaultOptions.masterkey.forgetSavedPasswordBtn=Forget Saved Password
 vaultOptions.masterkey.recoveryKeyExpanation=A recovery key is your only means to restore access to a vault if you lose your password.
 vaultOptions.masterkey.showRecoveryKeyBtn=Display Recovery Key
 vaultOptions.masterkey.recoverPasswordBtn=Recover Password
+## Auto Lock
+vaultOptions.autoLock=Auto-Lock
+vaultOptions.autoLock.lockAfterTimePart1=Lock when idle for
+vaultOptions.autoLock.lockAfterTimePart2=minutes
 
 # Recovery Key
 recoveryKey.title=Recovery Key