Merge pull request #1603 from cryptomator/feature/autoLock

fixes #274
This commit is contained in:
Sebastian Stenzel
2021-06-02 16:24:26 +02:00
committed by GitHub
16 changed files with 238 additions and 23 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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