diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java b/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java new file mode 100644 index 000000000..a54c81b08 --- /dev/null +++ b/main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java @@ -0,0 +1,39 @@ +package org.cryptomator.common.settings; + +import org.apache.commons.lang3.SystemUtils; + +import java.util.Arrays; + +public enum KeychainBackend { + GNOME("preferences.general.keychainBackend.gnome", SystemUtils.IS_OS_LINUX), // + KDE("preferences.general.keychainBackend.kde", SystemUtils.IS_OS_LINUX), // + MAC_SYSTEM_KEYCHAIN("preferences.general.keychainBackend.macSystemKeychain", SystemUtils.IS_OS_MAC), // + WIN_SYSTEM_KEYCHAIN("preferences.general.keychainBackend.winSystemKeychain", SystemUtils.IS_OS_WINDOWS); + + public static KeychainBackend[] supportedBackends() { + return Arrays.stream(values()).filter(KeychainBackend::isSupported).toArray(KeychainBackend[]::new); + } + + public static KeychainBackend defaultBackend() { + if (SystemUtils.IS_OS_LINUX) { + return KeychainBackend.GNOME; + } else { // SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS + return Arrays.stream(KeychainBackend.supportedBackends()).findFirst().orElseThrow(IllegalStateException::new); + } + } + + private final String configName; + private final boolean isSupported; + + KeychainBackend(String configName, boolean isSupported) { + this.configName = configName; + this.isSupported = isSupported; + } + + public String getDisplayName() { + return configName; + } + + public boolean isSupported() { return isSupported; } + +} diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java index 16d16b01d..3a6cb1f25 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/Settings.java @@ -36,6 +36,7 @@ public class Settings { public static final boolean DEFAULT_DEBUG_MODE = false; public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE; public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT; + public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = KeychainBackend.defaultBackend(); public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT; private static final String DEFAULT_LICENSE_KEY = ""; @@ -49,6 +50,7 @@ public class Settings { private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE); private final ObjectProperty preferredVolumeImpl = new SimpleObjectProperty<>(DEFAULT_PREFERRED_VOLUME_IMPL); private final ObjectProperty theme = new SimpleObjectProperty<>(DEFAULT_THEME); + private final ObjectProperty keychainBackend = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_BACKEND); private final ObjectProperty userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION); private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY); @@ -68,6 +70,7 @@ public class Settings { debugMode.addListener(this::somethingChanged); preferredVolumeImpl.addListener(this::somethingChanged); theme.addListener(this::somethingChanged); + keychainBackend.addListener(this::somethingChanged); userInterfaceOrientation.addListener(this::somethingChanged); licenseKey.addListener(this::somethingChanged); } @@ -128,6 +131,8 @@ public class Settings { return theme; } + public ObjectProperty keychainBackend() { return keychainBackend; } + public ObjectProperty userInterfaceOrientation() { return userInterfaceOrientation; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java index 395034f56..b1ff100c9 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java @@ -38,6 +38,7 @@ public class SettingsJsonAdapter extends TypeAdapter { out.name("preferredVolumeImpl").value(value.preferredVolumeImpl().get().name()); out.name("theme").value(value.theme().get().name()); out.name("uiOrientation").value(value.userInterfaceOrientation().get().name()); + out.name("keychainBackend").value(value.keychainBackend().get().name()); out.name("licenseKey").value(value.licenseKey().get()); out.endObject(); } @@ -69,6 +70,7 @@ public class SettingsJsonAdapter extends TypeAdapter { case "preferredVolumeImpl" -> settings.preferredVolumeImpl().set(parsePreferredVolumeImplName(in.nextString())); case "theme" -> settings.theme().set(parseUiTheme(in.nextString())); case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString())); + case "keychainBackend" -> settings.keychainBackend().set(parseKeychainBackend(in.nextString())); case "licenseKey" -> settings.licenseKey().set(in.nextString()); default -> { LOG.warn("Unsupported vault setting found in JSON: " + name); @@ -108,6 +110,15 @@ public class SettingsJsonAdapter extends TypeAdapter { } } + private KeychainBackend parseKeychainBackend(String backendName) { + try { + return KeychainBackend.valueOf(backendName.toUpperCase()); + } catch (IllegalArgumentException e) { + LOG.warn("Invalid keychain backend {}. Defaulting to {}.", backendName, Settings.DEFAULT_KEYCHAIN_BACKEND); + return Settings.DEFAULT_KEYCHAIN_BACKEND; + } + } + private NodeOrientation parseUiOrientation(String uiOrientationName) { try { return NodeOrientation.valueOf(uiOrientationName.toUpperCase()); diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java index abd50287e..553ce4eab 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java @@ -5,7 +5,7 @@ *******************************************************************************/ package org.cryptomator.keychain; -interface KeychainAccessStrategy { +public interface KeychainAccessStrategy { /** * Associates a passphrase with a given key. diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java index e75d387cb..f49ea5d1d 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java @@ -1,9 +1,13 @@ package org.cryptomator.keychain; +import javafx.beans.property.ObjectProperty; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.KeychainBackend; +import org.cryptomator.common.settings.Settings; import javax.inject.Inject; import javax.inject.Singleton; +import java.util.EnumSet; import java.util.Optional; /** @@ -16,25 +20,65 @@ public class LinuxSystemKeychainAccess implements KeychainAccessStrategy { // the actual implementation is hidden in this delegate objects which are loaded via reflection, // as it depends on libraries that aren't necessarily available: private final Optional delegate; + private final Settings settings; + private static EnumSet availableKeychainBackends = EnumSet.noneOf(KeychainBackend.class); + private static KeychainBackend backendActivated = null; + private static boolean isGnomeKeyringAvailable; + private static boolean isKdeWalletAvailable; @Inject - public LinuxSystemKeychainAccess() { + public LinuxSystemKeychainAccess(Settings settings) { + this.settings = settings; this.delegate = constructKeychainAccess(); } - private static Optional constructKeychainAccess() { - try { // is gnome-keyring or kwallet installed? + private Optional constructKeychainAccess() { + try { // find out which backends are available Class clazz = Class.forName("org.cryptomator.keychain.LinuxSecretServiceKeychainAccessImpl"); - KeychainAccessStrategy instance = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance(); - if (instance.isSupported()) return Optional.of(instance); + KeychainAccessStrategy gnomeKeyring = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance(); + if (gnomeKeyring.isSupported()) { + LinuxSystemKeychainAccess.availableKeychainBackends.add(KeychainBackend.GNOME); + LinuxSystemKeychainAccess.isGnomeKeyringAvailable = true; + } clazz = Class.forName("org.cryptomator.keychain.LinuxKDEWalletKeychainAccessImpl"); - instance = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance(); - return Optional.of(instance); + KeychainAccessStrategy kdeWallet = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance(); + if (kdeWallet.isSupported()) { + LinuxSystemKeychainAccess.availableKeychainBackends.add(KeychainBackend.KDE); + LinuxSystemKeychainAccess.isKdeWalletAvailable = true; + } + + // load password backend setting as the preferred backend + ObjectProperty pwSetting = settings.keychainBackend(); + + // check for GNOME keyring first, as this gets precedence over + // KDE wallet as the former was implemented first + if (isGnomeKeyringAvailable && pwSetting.get().equals(KeychainBackend.GNOME)) { + pwSetting.setValue(KeychainBackend.GNOME); + LinuxSystemKeychainAccess.backendActivated = KeychainBackend.GNOME; + return Optional.of(gnomeKeyring); + } + + if (isKdeWalletAvailable && pwSetting.get().equals(KeychainBackend.KDE)) { + pwSetting.setValue(KeychainBackend.KDE); + LinuxSystemKeychainAccess.backendActivated = KeychainBackend.KDE; + return Optional.of(kdeWallet); + } + return Optional.empty(); } catch (Exception e) { return Optional.empty(); } } + /* Getter/Setter */ + + public static EnumSet getAvailableKeychainBackends() { + return availableKeychainBackends; + } + + public static KeychainBackend getBackendActivated() { + return backendActivated; + } + @Override public boolean isSupported() { return SystemUtils.IS_OS_LINUX && delegate.map(KeychainAccessStrategy::isSupported).orElse(false); diff --git a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java index 3cf94d22e..0cdfd4a5a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java @@ -13,16 +13,22 @@ import javafx.scene.control.RadioButton; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.util.StringConverter; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.LicenseHolder; +import org.cryptomator.common.settings.KeychainBackend; import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.UiTheme; +import org.cryptomator.keychain.KeychainAccessStrategy; +import org.cryptomator.keychain.LinuxSystemKeychainAccess; import org.cryptomator.ui.common.FxController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; +import java.util.Arrays; +import java.util.EnumSet; import java.util.Optional; import java.util.ResourceBundle; import java.util.concurrent.ExecutorService; @@ -41,7 +47,9 @@ public class GeneralPreferencesController implements FxController { private final ResourceBundle resourceBundle; private final Application application; private final Environment environment; + private Optional keychain; public ChoiceBox themeChoiceBox; + public ChoiceBox keychainBackendChoiceBox; public CheckBox startHiddenCheckbox; public CheckBox debugModeCheckbox; public CheckBox autoStartCheckbox; @@ -50,10 +58,11 @@ public class GeneralPreferencesController implements FxController { public RadioButton nodeOrientationRtl; @Inject - GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) { + GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional autoStartStrategy, Optional keychain, ObjectProperty selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) { this.settings = settings; this.trayMenuSupported = trayMenuSupported; this.autoStartStrategy = autoStartStrategy; + this.keychain = keychain; this.selectedTabProperty = selectedTabProperty; this.licenseHolder = licenseHolder; this.executor = executor; @@ -84,6 +93,16 @@ public class GeneralPreferencesController implements FxController { nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT); nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT); nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation); + + keychainBackendChoiceBox.getItems().addAll(getAvailableBackends()); + if (keychain.isPresent() && SystemUtils.IS_OS_LINUX) { + keychainBackendChoiceBox.setValue(LinuxSystemKeychainAccess.getBackendActivated()); + } + if (keychain.isPresent() && (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)) { + keychainBackendChoiceBox.setValue(Arrays.stream(KeychainBackend.supportedBackends()).findFirst().orElseThrow(IllegalStateException::new)); + } + keychainBackendChoiceBox.setConverter(new KeychainBackendConverter(resourceBundle)); + keychainBackendChoiceBox.valueProperty().bindBidirectional(settings.keychainBackend()); } public boolean isTrayMenuSupported() { @@ -153,6 +172,25 @@ public class GeneralPreferencesController implements FxController { } } + private static class KeychainBackendConverter extends StringConverter { + + private final ResourceBundle resourceBundle; + + KeychainBackendConverter(ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + @Override + public String toString(KeychainBackend impl) { + return resourceBundle.getString(impl.getDisplayName()); + } + + @Override + public KeychainBackend fromString(String string) { + throw new UnsupportedOperationException(); + } + } + private static class ToggleAutoStartTask extends Task { private final AutoStartStrategy autoStart; @@ -176,4 +214,17 @@ public class GeneralPreferencesController implements FxController { } } + private KeychainBackend[] getAvailableBackends() { + if (!keychain.isPresent()) { + return new KeychainBackend[]{}; + } + if (SystemUtils.IS_OS_LINUX) { + EnumSet backends = LinuxSystemKeychainAccess.getAvailableKeychainBackends(); + return backends.toArray(KeychainBackend[]::new); + } + if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) { + return KeychainBackend.supportedBackends(); + } + return new KeychainBackend[]{}; + } } diff --git a/main/ui/src/main/resources/fxml/preferences_general.fxml b/main/ui/src/main/resources/fxml/preferences_general.fxml index e3ace69f6..0e19ac470 100644 --- a/main/ui/src/main/resources/fxml/preferences_general.fxml +++ b/main/ui/src/main/resources/fxml/preferences_general.fxml @@ -39,6 +39,11 @@ + + + diff --git a/main/ui/src/main/resources/i18n/strings.properties b/main/ui/src/main/resources/i18n/strings.properties index 18208a9bb..c92e5fa1f 100644 --- a/main/ui/src/main/resources/i18n/strings.properties +++ b/main/ui/src/main/resources/i18n/strings.properties @@ -143,6 +143,11 @@ preferences.general.startHidden=Hide window when starting Cryptomator preferences.general.debugLogging=Enable debug logging preferences.general.debugDirectory=Reveal log files preferences.general.autoStart=Launch Cryptomator on system start +preferences.general.keychainBackend=Password backend +preferences.general.keychainBackend.gnome=Gnome Keyring +preferences.general.keychainBackend.kde=KDE KWallet +preferences.general.keychainBackend.macSystemKeychain=macOS Keychain Access +preferences.general.keychainBackend.winSystemKeychain=Windows Data Protection Keychain preferences.general.interfaceOrientation=Interface Orientation preferences.general.interfaceOrientation.ltr=Left to Right preferences.general.interfaceOrientation.rtl=Right to Left