diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index f8e0e8078..705f98b8f 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/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 provideVaultList(VaultListManager vaultListManager) { - return vaultListManager.getVaultList(); - } - @Provides @Singleton static ScheduledExecutorService provideScheduledExecutorService(ShutdownHook shutdownHook) { diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 03aef5629..10a023806 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -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 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 diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index d68a67e0b..0cf92a48b 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -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; } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java b/main/commons/src/main/java/org/cryptomator/common/vaults/AutoLocker.java new file mode 100644 index 000000000..da584086d --- /dev/null +++ b/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 vaultList; + + @Inject + public AutoLocker(ScheduledExecutorService scheduler, ObservableList 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; + } + } + + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index d5038630a..05ea8ce07 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -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 vaultList; private final String defaultVaultName; @Inject - public VaultListManager(VaultComponent.Builder vaultComponentBuilder, ResourceBundle resourceBundle, Settings settings) { + public VaultListManager(ObservableList 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 getVaultList() { - return vaultList; + autoLocker.init(); } public Vault add(Path pathToVault) throws IOException { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListModule.java new file mode 100644 index 000000000..644d05335 --- /dev/null +++ b/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 provideVaultList() { + return FXCollections.observableArrayList(Vault::observables); + } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java index 6dc86e8be..f6744ec6b 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultStats.java @@ -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 lastActivity = new SimpleObjectProperty<>(); @Inject VaultStats(AtomicReference 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 lastActivityProperty() { + return lastActivity; + } + + public Instant getLastActivity() { + return lastActivity.get(); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index 13c583c0e..a273447bb 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/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"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java index b0ea8da47..02a2e5b9e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/fxapp/FxApplication.java @@ -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 { diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/AutoLockVaultOptionsController.java new file mode 100644 index 000000000..1bb74690f --- /dev/null +++ b/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 { + + @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; + } + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java index 86945374a..03a4922d4 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/SelectedVaultOptionsTab.java +++ b/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, } diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java index 15879e316..20dac7594 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsController.java +++ b/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 selectedTabProperty) { @@ -47,6 +48,7 @@ public class VaultOptionsController implements FxController { case ANY, GENERAL -> generalTab; case MOUNT -> mountTab; case KEY -> keyTab; + case AUTOLOCK -> autoLockTab; }; } diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java index fb248f38e..89cef234b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java +++ b/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); + } diff --git a/main/ui/src/main/resources/fxml/vault_options.fxml b/main/ui/src/main/resources/fxml/vault_options.fxml index 2229f5709..4808209b2 100644 --- a/main/ui/src/main/resources/fxml/vault_options.fxml +++ b/main/ui/src/main/resources/fxml/vault_options.fxml @@ -36,5 +36,13 @@ + + + + + + + + diff --git a/main/ui/src/main/resources/fxml/vault_options_autolock.fxml b/main/ui/src/main/resources/fxml/vault_options_autolock.fxml new file mode 100644 index 000000000..8d63b792d --- /dev/null +++ b/main/ui/src/main/resources/fxml/vault_options_autolock.fxml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/ui/src/main/resources/i18n/strings.properties b/main/ui/src/main/resources/i18n/strings.properties index 7ff081f5a..8ffc15c4c 100644 --- a/main/ui/src/main/resources/i18n/strings.properties +++ b/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