From a8a57c2ab814ec635c3f098ffef32c1a172d60ce Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 25 Oct 2025 16:33:38 +0200 Subject: [PATCH 1/9] Replace GnomeKeyringKeychainAccess, KDEWalletKeychainAccess by SecretServiceKeychainAccess --- pom.xml | 10 +- src/main/java/module-info.java | 8 +- .../keychain/GnomeKeyringKeychainAccess.java | 103 --------- .../keychain/KDEWalletKeychainAccess.java | 195 ------------------ .../keychain/SecretServiceKeychainAccess.java | 130 ++++++++++++ ...tegrations.keychain.KeychainAccessProvider | 3 +- .../keychain/KDEWalletKeychainAccessTest.java | 45 ---- ...a => SecretServiceKeychainAccessTest.java} | 30 +-- 8 files changed, 151 insertions(+), 373 deletions(-) delete mode 100644 src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java delete mode 100644 src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java create mode 100644 src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java delete mode 100644 src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java rename src/test/java/org/cryptomator/linux/keychain/{GnomeKeyringKeychainAccessTest.java => SecretServiceKeychainAccessTest.java} (69%) diff --git a/pom.xml b/pom.xml index b55dfa6..27f1198 100644 --- a/pom.xml +++ b/pom.xml @@ -41,8 +41,7 @@ 1.7.0 - 2.0.1-alpha - 1.4.0 + 1.0.0-SNAPSHOT 2.0.17 1.4.2 @@ -73,15 +72,10 @@ ${slf4j.version} - de.swiesend + org.purejava secret-service ${secret-service.version} - - org.purejava - kdewallet - ${kdewallet.version} - org.purejava diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c728d22..00f2dee 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -4,8 +4,7 @@ import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.linux.autostart.FreedesktopAutoStartService; -import org.cryptomator.linux.keychain.KDEWalletKeychainAccess; -import org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess; +import org.cryptomator.linux.keychain.SecretServiceKeychainAccess; import org.cryptomator.linux.quickaccess.DolphinPlaces; import org.cryptomator.linux.quickaccess.NautilusBookmarks; import org.cryptomator.linux.revealpath.DBusSendRevealPathService; @@ -16,12 +15,11 @@ requires org.slf4j; requires org.freedesktop.dbus; requires org.purejava.appindicator; - requires org.purejava.kwallet; - requires de.swiesend.secretservice; + requires org.purejava.secret; requires java.xml; provides AutoStartProvider with FreedesktopAutoStartService; - provides KeychainAccessProvider with GnomeKeyringKeychainAccess, KDEWalletKeychainAccess; + provides KeychainAccessProvider with SecretServiceKeychainAccess; provides RevealPathService with DBusSendRevealPathService; provides TrayMenuController with AppindicatorTrayMenuController; provides QuickAccessService with NautilusBookmarks, DolphinPlaces; diff --git a/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java deleted file mode 100644 index a7a2a91..0000000 --- a/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.cryptomator.linux.keychain; - -import de.swiesend.secretservice.simple.SimpleCollection; -import org.cryptomator.integrations.common.DisplayName; -import org.cryptomator.integrations.common.OperatingSystem; -import org.cryptomator.integrations.common.Priority; -import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.integrations.keychain.KeychainAccessProvider; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -@Priority(900) -@OperatingSystem(OperatingSystem.Value.LINUX) -@DisplayName("GNOME Keyring") -public class GnomeKeyringKeychainAccess implements KeychainAccessProvider { - - private static final Logger LOG = LoggerFactory.getLogger(GnomeKeyringKeychainAccess.class); - - private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator"; - - @Override - public boolean isSupported() { - try { - return SimpleCollection.isGnomeKeyringAvailable(); - } catch (RuntimeException e) { - LOG.warn("Initializing secret service keychain access failed", e); - return false; - } catch (ExceptionInInitializerError err) { - LOG.warn("Initializing secret service keychain access failed", err.getException()); - return false; - } - } - - @Override - public boolean isLocked() { - try (SimpleCollection keyring = new SimpleCollection()) { - return keyring.isLocked(); - } catch (IOException e) { - return true; - } - } - - @Override - public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - try (SimpleCollection keyring = new SimpleCollection()) { - List list = keyring.getItems(createAttributes(key)); - if (list == null || list.isEmpty()) { - keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key)); - } else { - changePassphrase(key, displayName, passphrase); - } - } catch (IOException | SecurityException e) { - throw new KeychainAccessException("Storing password failed.", e); - } - } - - @Override - public char[] loadPassphrase(String key) throws KeychainAccessException { - try (SimpleCollection keyring = new SimpleCollection()) { - List list = keyring.getItems(createAttributes(key)); - if (list != null && !list.isEmpty()) { - return keyring.getSecret(list.get(0)); - } else { - return null; - } - } catch (IOException | SecurityException e) { - throw new KeychainAccessException("Loading password failed.", e); - } - } - - @Override - public void deletePassphrase(String key) throws KeychainAccessException { - try (SimpleCollection keyring = new SimpleCollection()) { - List list = keyring.getItems(createAttributes(key)); - if (list != null && !list.isEmpty()) { - keyring.deleteItem(list.get(0)); - } - } catch (IOException | SecurityException e) { - throw new KeychainAccessException("Deleting password failed.", e); - } - } - - @Override - public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - try (SimpleCollection keyring = new SimpleCollection()) { - List list = keyring.getItems(createAttributes(key)); - if (list != null && !list.isEmpty()) { - keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key)); - } - } catch (IOException | SecurityException e) { - throw new KeychainAccessException("Changing password failed.", e); - } - } - - private Map createAttributes(String key) { - return Map.of("Vault", key); - } - -} diff --git a/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java deleted file mode 100644 index 5db6fa8..0000000 --- a/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java +++ /dev/null @@ -1,195 +0,0 @@ -package org.cryptomator.linux.keychain; - -import org.cryptomator.integrations.common.DisplayName; -import org.cryptomator.integrations.common.OperatingSystem; -import org.cryptomator.integrations.common.Priority; -import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.integrations.keychain.KeychainAccessProvider; -import org.cryptomator.linux.util.CheckUtil; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; -import org.freedesktop.dbus.exceptions.DBusConnectionException; -import org.freedesktop.dbus.exceptions.DBusException; -import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.purejava.kwallet.KWallet; -import org.purejava.kwallet.KDEWallet; -import org.purejava.kwallet.Static; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Optional; - -@Priority(900) -@OperatingSystem(OperatingSystem.Value.LINUX) -@DisplayName("KDE Wallet") -public class KDEWalletKeychainAccess implements KeychainAccessProvider { - - private static final Logger LOG = LoggerFactory.getLogger(KDEWalletKeychainAccess.class); - private static final String FOLDER_NAME = "Cryptomator"; - private static final String APP_NAME = "Cryptomator"; - - private final Optional wallet; - - public KDEWalletKeychainAccess() { - this.wallet = ConnectedWallet.connect(); - } - - @Override - public boolean isSupported() { - return wallet.map(ConnectedWallet::isSupported).orElse(false); - } - - @Override - public boolean isLocked() { - return wallet.map(ConnectedWallet::isLocked).orElse(false); - } - - @Override - public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); - wallet.get().storePassphrase(key, passphrase); - } - - @Override - public char[] loadPassphrase(String key) throws KeychainAccessException { - CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); - return wallet.get().loadPassphrase(key); - } - - @Override - public void deletePassphrase(String key) throws KeychainAccessException { - CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); - wallet.get().deletePassphrase(key); - } - - @Override - public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); - wallet.get().changePassphrase(key, passphrase); - } - - private static class ConnectedWallet { - - private final KDEWallet wallet; - private int handle = -1; - - public ConnectedWallet(DBusConnection connection) { - this.wallet = new KDEWallet(connection); - } - - static Optional connect() { - try { - return Optional.of(new ConnectedWallet(getNewConnection())); - } catch (DBusException e) { - LOG.warn("Connecting to D-Bus failed.", e); - return Optional.empty(); - } - } - - private static DBusConnection getNewConnection() throws DBusException { - try { - return DBusConnectionBuilder.forSessionBus().withShared(false).build(); - } catch (DBusConnectionException | DBusExecutionException de) { - LOG.warn("Connecting to SESSION bus failed.", de); - LOG.warn("Falling back to SYSTEM DBus"); - return DBusConnectionBuilder.forSystemBus().build(); - } - } - - public boolean isSupported() { - try { - return wallet.isEnabled(); - } catch (RuntimeException e) { - LOG.warn("Failed to check if KDE Wallet is available.", e); - return false; - } - } - - public boolean isLocked() { - try { - return !wallet.isOpen(Static.DEFAULT_WALLET); - } catch (RuntimeException e) { - LOG.warn("Failed to check whether KDE Wallet is open, therefore considering it closed.", e); - return true; - } - } - - public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - try { - if (walletIsOpen() && - !(wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1) - && wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) { - LOG.debug("Passphrase successfully stored."); - } else { - LOG.debug("Passphrase was not stored."); - } - } catch (RuntimeException e) { - throw new KeychainAccessException("Storing the passphrase failed.", e); - } - } - - public char[] loadPassphrase(String key) throws KeychainAccessException { - String password = ""; - try { - if (walletIsOpen()) { - password = wallet.readPassword(handle, FOLDER_NAME, key, APP_NAME); - LOG.debug("loadPassphrase: wallet is open."); - } else { - LOG.debug("loadPassphrase: wallet is closed."); - } - return (password.isEmpty()) ? null : password.toCharArray(); - } catch (RuntimeException e) { - throw new KeychainAccessException("Loading the passphrase failed.", e); - } - } - - public void deletePassphrase(String key) throws KeychainAccessException { - try { - if (walletIsOpen() - && wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) - && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 - && wallet.removeEntry(handle, FOLDER_NAME, key, APP_NAME) == 0) { - LOG.debug("Passphrase successfully deleted."); - } else { - LOG.debug("Passphrase was not deleted."); - } - } catch (RuntimeException e) { - throw new KeychainAccessException("Deleting the passphrase failed.", e); - } - } - - public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { - try { - if (walletIsOpen() - && wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) - && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 - && wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) { - LOG.debug("Passphrase successfully changed."); - } else { - LOG.debug("Passphrase could not be changed."); - } - } catch (RuntimeException e) { - throw new KeychainAccessException("Changing the passphrase failed.", e); - } - } - - private boolean walletIsOpen() throws KeychainAccessException { - try { - if (wallet.isOpen(Static.DEFAULT_WALLET)) { - // This is needed due to KeechainManager loading the passphase directly - if (handle == -1) handle = wallet.open(Static.DEFAULT_WALLET, 0, APP_NAME); - return true; - } - wallet.openAsync(Static.DEFAULT_WALLET, 0, APP_NAME, false); - wallet.getSignalHandler().await(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5, () -> null); - handle = wallet.getSignalHandler().getLastHandledSignal(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5).handle; - LOG.debug("Wallet successfully initialized."); - return handle != -1; - } catch (RuntimeException e) { - throw new KeychainAccessException("Asynchronous opening the wallet failed.", e); - } - } - - - } -} diff --git a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java new file mode 100644 index 0000000..b849f3f --- /dev/null +++ b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java @@ -0,0 +1,130 @@ +package org.cryptomator.linux.keychain; + +import org.cryptomator.integrations.common.DisplayName; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.keychain.KeychainAccessException; +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.freedesktop.dbus.DBusPath; +import org.purejava.secret.api.Collection; +import org.purejava.secret.api.EncryptedSession; +import org.purejava.secret.api.Item; +import org.purejava.secret.api.Static; + +import java.util.Map; + +@Priority(900) +@OperatingSystem(OperatingSystem.Value.LINUX) +@DisplayName("Secret Service") +public class SecretServiceKeychainAccess implements KeychainAccessProvider { + + private final EncryptedSession session = new EncryptedSession(); + private final Collection collection = new Collection(new DBusPath(Static.DBusPath.DEFAULT_COLLECTION)); + + @Override + public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + try { + var call = collection.searchItems(createAttributes(key)); + if (call.isSuccess()) { + if (call.value().isEmpty()) { + var itemProps = Item.createProperties(displayName, createAttributes(key)); + var secret = session.encrypt(passphrase); + var created = collection.createItem(itemProps, secret, false); + if (!created.isSuccess()) { + throw new KeychainAccessException("Storing password failed", created.error()); + } + } else { + changePassphrase(key, displayName, passphrase); + } + } else { + throw new KeychainAccessException("Storing password failed", call.error()); + } + } catch (Exception e) { + throw new KeychainAccessException("Storing password failed.", e); + } + } + + @Override + public char[] loadPassphrase(String key) throws KeychainAccessException { + try { + var call = collection.searchItems(createAttributes(key)); + if (call.isSuccess()) { + if (!call.value().isEmpty()) { + var path = call.value().getFirst(); + var secret = new Item(path).getSecret(session.getSession()); + return session.decrypt(secret); + } else { + return null; + } + } else { + throw new KeychainAccessException("Loading password failed", call.error()); + } + } catch (Exception e) { + throw new KeychainAccessException("Loading password failed.", e); + } + } + + @Override + public void deletePassphrase(String key) throws KeychainAccessException { + try { + var call = collection.searchItems(createAttributes(key)); + if (call.isSuccess()) { + if (!call.value().isEmpty()) { + var path = call.value().getFirst(); + var item = new Item(path); + var deleted = item.delete(); + if (!deleted.isSuccess()) { + throw new KeychainAccessException("Deleting password failed", deleted.error()); + } + } else { + var msg = "Vault " + key + " not found, deleting failed"; + throw new KeychainAccessException(msg); + } + } else { + throw new KeychainAccessException("Deleting password failed", call.error()); + } + } catch (Exception e) { + throw new KeychainAccessException("Deleting password failed", e); + } + } + + @Override + public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + try { + var call = collection.searchItems(createAttributes(key)); + if (call.isSuccess()) { + if (!call.value().isEmpty()) { + var secret = session.encrypt(passphrase); + var itemProps = Item.createProperties(displayName, createAttributes(key)); + var updated = collection.createItem(itemProps, secret, true); + if (!updated.isSuccess()) { + throw new KeychainAccessException("Updating password failed", updated.error()); + } + } else { + var msg = "Vault " + key + " not found, updating failed"; + throw new KeychainAccessException(msg); + } + } else { + throw new KeychainAccessException("Updating password failed", call.error()); + } + } catch (Exception e) { + throw new KeychainAccessException("Updating password failed", e); + } + } + + @Override + public boolean isSupported() { + return session.setupEncryptedSession() && + session.getService().hasDefaultCollection(); + } + + @Override + public boolean isLocked() { + var call = collection.isLocked(); + return call.isSuccess() && call.value(); + } + + private Map createAttributes(String key) { + return Map.of("Vault", key); + } +} diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider index e68909b..6de442f 100644 --- a/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider +++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider @@ -1,2 +1 @@ -org.cryptomator.linux.keychain.KDEWalletKeychainAccess -org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess \ No newline at end of file +org.cryptomator.linux.keychain.SecretServiceKeychainAccess \ No newline at end of file diff --git a/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java deleted file mode 100644 index f845dde..0000000 --- a/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.cryptomator.linux.keychain; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.TimeUnit; - -/** - * Unit tests for KWallet access via DBUS. - */ -@EnabledOnOs(OS.LINUX) -@EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".*") -public class KDEWalletKeychainAccessTest { - - private static boolean isInstalled; - - @BeforeAll - public static void checkSystemAndSetup() throws IOException { - ProcessBuilder dbusSend = new ProcessBuilder("dbus-send", "--print-reply", "--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListActivatableNames"); - ProcessBuilder grep = new ProcessBuilder("grep", "-q", "org.kde.kwallet"); - try { - Process end = ProcessBuilder.startPipeline(List.of(dbusSend, grep)).get(1); - if (end.waitFor(1000, TimeUnit.MILLISECONDS)) { - isInstalled = end.exitValue() == 0; - } else { - isInstalled = false; - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - - - @Test - public void testIsSupported() { - KDEWalletKeychainAccess keychainAccess = new KDEWalletKeychainAccess(); - Assertions.assertEquals(isInstalled, keychainAccess.isSupported()); - } -} diff --git a/src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java similarity index 69% rename from src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java rename to src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java index 93aa7e0..c52596e 100644 --- a/src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java +++ b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java @@ -17,17 +17,17 @@ import java.util.concurrent.TimeUnit; /** - * Unit tests for GNOME keyring access via DBUS. + * Unit tests for Secret Service access via Dbus. */ @EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".*") -public class GnomeKeyringKeychainAccessTest { +public class SecretServiceKeychainAccessTest { private static boolean isInstalled; @BeforeAll public static void checkSystemAndSetup() throws IOException { ProcessBuilder dbusSend = new ProcessBuilder("dbus-send", "--print-reply", "--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListNames"); - ProcessBuilder grep = new ProcessBuilder("grep", "-q", "org.gnome.keyring"); + ProcessBuilder grep = new ProcessBuilder("grep", "-q", "org.freedesktop.secrets"); try { Process end = ProcessBuilder.startPipeline(List.of(dbusSend, grep)).get(1); if (end.waitFor(1000, TimeUnit.MILLISECONDS)) { @@ -42,28 +42,28 @@ public static void checkSystemAndSetup() throws IOException { @Test public void testIsSupported() { - var gnomeKeyring = new GnomeKeyringKeychainAccess(); - Assertions.assertEquals(isInstalled, gnomeKeyring.isSupported()); + var service = new SecretServiceKeychainAccess(); + Assertions.assertEquals(isInstalled, service.isSupported()); } @Nested @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - @EnabledIf("gnomeKeyringAvailableAndUnlocked") + @EnabledIf("serviceAvailableAndUnlocked") class FunctionalTests { static final String KEY_ID = "cryptomator-test-" + UUID.randomUUID(); - final GnomeKeyringKeychainAccess gnomeKeyring = new GnomeKeyringKeychainAccess(); + final SecretServiceKeychainAccess keyring = new SecretServiceKeychainAccess(); @Test @Order(1) public void testStore() throws KeychainAccessException { - gnomeKeyring.storePassphrase(KEY_ID, "cryptomator-test", "p0ssw0rd"); + keyring.storePassphrase(KEY_ID, "cryptomator-test", "p0ssw0rd"); } @Test @Order(2) public void testLoad() throws KeychainAccessException { - var passphrase = gnomeKeyring.loadPassphrase(KEY_ID); + var passphrase = keyring.loadPassphrase(KEY_ID); Assertions.assertNotNull(passphrase); Assertions.assertEquals("p0ssw0rd", String.copyValueOf(passphrase)); } @@ -71,20 +71,20 @@ public void testLoad() throws KeychainAccessException { @Test @Order(3) public void testDelete() throws KeychainAccessException { - gnomeKeyring.deletePassphrase(KEY_ID); + keyring.deletePassphrase(KEY_ID); } @Test @Order(4) public void testLoadNotExisting() throws KeychainAccessException { - var result = gnomeKeyring.loadPassphrase(KEY_ID); + var result = keyring.loadPassphrase(KEY_ID); Assertions.assertNull(result); } - public static boolean gnomeKeyringAvailableAndUnlocked() { - var secretServiceKeychain = new GnomeKeyringKeychainAccess(); - return secretServiceKeychain.isSupported() && !secretServiceKeychain.isLocked(); + public static boolean serviceAvailableAndUnlocked() { + var service = new SecretServiceKeychainAccess(); + return service.isSupported() && !service.isLocked(); } } -} \ No newline at end of file +} From 13e31322470051bc969d5568bd8bc73ba392ad39 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 25 Oct 2025 18:58:31 +0200 Subject: [PATCH 2/9] Encrypted session is needed for tests to work --- .../keychain/SecretServiceKeychainAccessTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java index c52596e..b07b869 100644 --- a/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java +++ b/src/test/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccessTest.java @@ -52,18 +52,19 @@ public void testIsSupported() { class FunctionalTests { static final String KEY_ID = "cryptomator-test-" + UUID.randomUUID(); - final SecretServiceKeychainAccess keyring = new SecretServiceKeychainAccess(); + final static SecretServiceKeychainAccess KEYRING = new SecretServiceKeychainAccess(); @Test @Order(1) public void testStore() throws KeychainAccessException { - keyring.storePassphrase(KEY_ID, "cryptomator-test", "p0ssw0rd"); + KEYRING.isSupported(); // ensure encrypted session + KEYRING.storePassphrase(KEY_ID, "cryptomator-test", "p0ssw0rd"); } @Test @Order(2) public void testLoad() throws KeychainAccessException { - var passphrase = keyring.loadPassphrase(KEY_ID); + var passphrase = KEYRING.loadPassphrase(KEY_ID); Assertions.assertNotNull(passphrase); Assertions.assertEquals("p0ssw0rd", String.copyValueOf(passphrase)); } @@ -71,13 +72,13 @@ public void testLoad() throws KeychainAccessException { @Test @Order(3) public void testDelete() throws KeychainAccessException { - keyring.deletePassphrase(KEY_ID); + KEYRING.deletePassphrase(KEY_ID); } @Test @Order(4) public void testLoadNotExisting() throws KeychainAccessException { - var result = keyring.loadPassphrase(KEY_ID); + var result = KEYRING.loadPassphrase(KEY_ID); Assertions.assertNull(result); } From 5163d8b25753a06db48521c593e6d0c4e46c5dbc Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 26 Oct 2025 10:14:53 +0100 Subject: [PATCH 3/9] Ensure unlocking Add handlers --- .../keychain/SecretServiceKeychainAccess.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java index b849f3f..a43748a 100644 --- a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java +++ b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java @@ -10,7 +10,11 @@ import org.purejava.secret.api.EncryptedSession; import org.purejava.secret.api.Item; import org.purejava.secret.api.Static; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Map; @Priority(900) @@ -18,15 +22,28 @@ @DisplayName("Secret Service") public class SecretServiceKeychainAccess implements KeychainAccessProvider { + private static final Logger LOG = LoggerFactory.getLogger(SecretServiceKeychainAccess.class); private final EncryptedSession session = new EncryptedSession(); private final Collection collection = new Collection(new DBusPath(Static.DBusPath.DEFAULT_COLLECTION)); + public SecretServiceKeychainAccess() { + session.getService().addCollectionChangedHandler(collection -> LOG.debug("Collection {} changed", collection.getPath())); + session.getService().addCollectionCreatedHandler(collection -> LOG.debug("Collection {} created", collection.getPath())); + session.getService().addCollectionDeletedHandler(collection -> LOG.debug("Collection {} deleted", collection.getPath())); + collection.addItemChangedHandler(item -> LOG.debug("Item {} changed", item.getPath())); + collection.addItemCreatedHandler(item -> LOG.debug("Item {} created", item.getPath())); + collection.addItemDeletedHandler(item -> LOG.debug("Item {} deleted", item.getPath())); + } + @Override public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { try { var call = collection.searchItems(createAttributes(key)); if (call.isSuccess()) { if (call.value().isEmpty()) { + List lockable = new ArrayList<>(); + lockable.add(new DBusPath(collection.getDBusPath())); + session.getService().unlock(lockable); var itemProps = Item.createProperties(displayName, createAttributes(key)); var secret = session.encrypt(passphrase); var created = collection.createItem(itemProps, secret, false); @@ -51,6 +68,7 @@ public char[] loadPassphrase(String key) throws KeychainAccessException { if (call.isSuccess()) { if (!call.value().isEmpty()) { var path = call.value().getFirst(); + session.getService().ensureUnlocked(path); var secret = new Item(path).getSecret(session.getSession()); return session.decrypt(secret); } else { @@ -71,6 +89,7 @@ public void deletePassphrase(String key) throws KeychainAccessException { if (call.isSuccess()) { if (!call.value().isEmpty()) { var path = call.value().getFirst(); + session.getService().ensureUnlocked(path); var item = new Item(path); var deleted = item.delete(); if (!deleted.isSuccess()) { @@ -94,6 +113,7 @@ public void changePassphrase(String key, String displayName, CharSequence passph var call = collection.searchItems(createAttributes(key)); if (call.isSuccess()) { if (!call.value().isEmpty()) { + session.getService().ensureUnlocked(call.value().getFirst()); var secret = session.encrypt(passphrase); var itemProps = Item.createProperties(displayName, createAttributes(key)); var updated = collection.createItem(itemProps, secret, true); From 9f227d0aa241f60e63e8e391a041d480c0ed7775 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 26 Oct 2025 13:51:40 +0100 Subject: [PATCH 4/9] Set default alias when missing --- .../linux/keychain/SecretServiceKeychainAccess.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java index a43748a..9cfe04a 100644 --- a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java +++ b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java @@ -30,6 +30,11 @@ public SecretServiceKeychainAccess() { session.getService().addCollectionChangedHandler(collection -> LOG.debug("Collection {} changed", collection.getPath())); session.getService().addCollectionCreatedHandler(collection -> LOG.debug("Collection {} created", collection.getPath())); session.getService().addCollectionDeletedHandler(collection -> LOG.debug("Collection {} deleted", collection.getPath())); + var getAlias = session.getService().readAlias("default"); + if (getAlias.isSuccess() && "/".equals(getAlias.value().getPath())) { + // default alias is not set; set it to the login keyring + session.getService().setAlias("default", new DBusPath(Static.DBusPath.LOGIN_COLLECTION)); + } collection.addItemChangedHandler(item -> LOG.debug("Item {} changed", item.getPath())); collection.addItemCreatedHandler(item -> LOG.debug("Item {} created", item.getPath())); collection.addItemDeletedHandler(item -> LOG.debug("Item {} deleted", item.getPath())); From c22f8138fb7b8523b9b9fd7f3be626642c151267 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sun, 26 Oct 2025 16:40:49 +0100 Subject: [PATCH 5/9] Migrate KDE wallet entries --- .../keychain/SecretServiceKeychainAccess.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java index 9cfe04a..62e858c 100644 --- a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java +++ b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java @@ -13,6 +13,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -38,6 +44,8 @@ public SecretServiceKeychainAccess() { collection.addItemChangedHandler(item -> LOG.debug("Item {} changed", item.getPath())); collection.addItemCreatedHandler(item -> LOG.debug("Item {} created", item.getPath())); collection.addItemDeletedHandler(item -> LOG.debug("Item {} deleted", item.getPath())); + + migrateKDEWalletEntries(); } @Override @@ -152,4 +160,33 @@ public boolean isLocked() { private Map createAttributes(String key) { return Map.of("Vault", key); } + + private void migrateKDEWalletEntries() { + session.setupEncryptedSession(); + var getItems = collection.getItems(); + if (getItems.isSuccess() && !getItems.value().isEmpty()) { + for (DBusPath i : getItems.value()) { + session.getService().ensureUnlocked(i); + var attribs = new Item(i).getAttributes(); + if (attribs.isSuccess() && + attribs.value().containsKey("server") && + attribs.value().containsKey("user") && + attribs.value().get("server").equals("Cryptomator")) { + + session.getService().ensureUnlocked(i); + var item = new Item(i); + var secret = item.getSecret(session.getSession()); + try { + storePassphrase(attribs.value().get("user"), "Cryptomator", new String(session.decrypt(secret))); + } catch (KeychainAccessException | NoSuchPaddingException | NoSuchAlgorithmException | + InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | + IllegalBlockSizeException e) { + LOG.error("Migrating entry {} for vault {} failed", i.getPath(), attribs.value().get("user")); + } + item.delete(); + LOG.info("Successfully migrated password for vault {}", attribs.value().get("user")); + } + } + } + } } From 161ea15cdaf502222777c070cd3217f29f97253c Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Mon, 27 Oct 2025 05:05:07 +0100 Subject: [PATCH 6/9] =?UTF-8?q?Service=201.0.0=20was=20released=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 27f1198..76369a0 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,7 @@ 1.7.0 - 1.0.0-SNAPSHOT + 1.0.0 2.0.17 1.4.2 From b9c6eb1724fc393fef6ee5b83f2ee765edc44a64 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Mon, 27 Oct 2025 18:52:57 +0100 Subject: [PATCH 7/9] Get back GnomeKeyringKeychainAccess, KDEWalletKeychainAccess --- pom.xml | 16 +- src/main/java/module-info.java | 7 +- .../keychain/GnomeKeyringKeychainAccess.java | 103 +++++++++ .../keychain/KDEWalletKeychainAccess.java | 196 ++++++++++++++++++ ...tegrations.keychain.KeychainAccessProvider | 4 +- .../GnomeKeyringKeychainAccessTest.java | 90 ++++++++ .../keychain/KDEWalletKeychainAccessTest.java | 45 ++++ 7 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java create mode 100644 src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java create mode 100644 src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java create mode 100644 src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java diff --git a/pom.xml b/pom.xml index 76369a0..5e81210 100644 --- a/pom.xml +++ b/pom.xml @@ -41,7 +41,9 @@ 1.7.0 - 1.0.0 + 2.0.1-alpha + 1.4.0 + 1.0.0 2.0.17 1.4.2 @@ -72,10 +74,20 @@ ${slf4j.version} - org.purejava + de.swiesend secret-service ${secret-service.version} + + org.purejava + kdewallet + ${kdewallet.version} + + + org.purejava + secret-service + ${secret-service-02.version} + org.purejava diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 00f2dee..facc5e2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -4,6 +4,8 @@ import org.cryptomator.integrations.revealpath.RevealPathService; import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.linux.autostart.FreedesktopAutoStartService; +import org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess; +import org.cryptomator.linux.keychain.KDEWalletKeychainAccess; import org.cryptomator.linux.keychain.SecretServiceKeychainAccess; import org.cryptomator.linux.quickaccess.DolphinPlaces; import org.cryptomator.linux.quickaccess.NautilusBookmarks; @@ -15,11 +17,14 @@ requires org.slf4j; requires org.freedesktop.dbus; requires org.purejava.appindicator; + requires org.purejava.kwallet; + requires de.swiesend.secretservice; requires org.purejava.secret; requires java.xml; + requires org.cryptomator.integrations.linux; provides AutoStartProvider with FreedesktopAutoStartService; - provides KeychainAccessProvider with SecretServiceKeychainAccess; + provides KeychainAccessProvider with SecretServiceKeychainAccess, GnomeKeyringKeychainAccess, KDEWalletKeychainAccess; provides RevealPathService with DBusSendRevealPathService; provides TrayMenuController with AppindicatorTrayMenuController; provides QuickAccessService with NautilusBookmarks, DolphinPlaces; diff --git a/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java new file mode 100644 index 0000000..a7a2a91 --- /dev/null +++ b/src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java @@ -0,0 +1,103 @@ +package org.cryptomator.linux.keychain; + +import de.swiesend.secretservice.simple.SimpleCollection; +import org.cryptomator.integrations.common.DisplayName; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.keychain.KeychainAccessException; +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@Priority(900) +@OperatingSystem(OperatingSystem.Value.LINUX) +@DisplayName("GNOME Keyring") +public class GnomeKeyringKeychainAccess implements KeychainAccessProvider { + + private static final Logger LOG = LoggerFactory.getLogger(GnomeKeyringKeychainAccess.class); + + private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator"; + + @Override + public boolean isSupported() { + try { + return SimpleCollection.isGnomeKeyringAvailable(); + } catch (RuntimeException e) { + LOG.warn("Initializing secret service keychain access failed", e); + return false; + } catch (ExceptionInInitializerError err) { + LOG.warn("Initializing secret service keychain access failed", err.getException()); + return false; + } + } + + @Override + public boolean isLocked() { + try (SimpleCollection keyring = new SimpleCollection()) { + return keyring.isLocked(); + } catch (IOException e) { + return true; + } + } + + @Override + public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + try (SimpleCollection keyring = new SimpleCollection()) { + List list = keyring.getItems(createAttributes(key)); + if (list == null || list.isEmpty()) { + keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key)); + } else { + changePassphrase(key, displayName, passphrase); + } + } catch (IOException | SecurityException e) { + throw new KeychainAccessException("Storing password failed.", e); + } + } + + @Override + public char[] loadPassphrase(String key) throws KeychainAccessException { + try (SimpleCollection keyring = new SimpleCollection()) { + List list = keyring.getItems(createAttributes(key)); + if (list != null && !list.isEmpty()) { + return keyring.getSecret(list.get(0)); + } else { + return null; + } + } catch (IOException | SecurityException e) { + throw new KeychainAccessException("Loading password failed.", e); + } + } + + @Override + public void deletePassphrase(String key) throws KeychainAccessException { + try (SimpleCollection keyring = new SimpleCollection()) { + List list = keyring.getItems(createAttributes(key)); + if (list != null && !list.isEmpty()) { + keyring.deleteItem(list.get(0)); + } + } catch (IOException | SecurityException e) { + throw new KeychainAccessException("Deleting password failed.", e); + } + } + + @Override + public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + try (SimpleCollection keyring = new SimpleCollection()) { + List list = keyring.getItems(createAttributes(key)); + if (list != null && !list.isEmpty()) { + keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key)); + } + } catch (IOException | SecurityException e) { + throw new KeychainAccessException("Changing password failed.", e); + } + } + + private Map createAttributes(String key) { + return Map.of("Vault", key); + } + +} diff --git a/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java new file mode 100644 index 0000000..3ce5433 --- /dev/null +++ b/src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java @@ -0,0 +1,196 @@ +package org.cryptomator.linux.keychain; + +import org.cryptomator.integrations.common.DisplayName; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.keychain.KeychainAccessException; +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.linux.util.CheckUtil; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; +import org.freedesktop.dbus.exceptions.DBusConnectionException; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.purejava.kwallet.KWallet; +import org.purejava.kwallet.KDEWallet; +import org.purejava.kwallet.Static; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +@Priority(900) +@OperatingSystem(OperatingSystem.Value.LINUX) +@DisplayName("KDE Wallet") +public class KDEWalletKeychainAccess implements KeychainAccessProvider { + + private static final Logger LOG = LoggerFactory.getLogger(KDEWalletKeychainAccess.class); + private static final String FOLDER_NAME = "Cryptomator"; + private static final String APP_NAME = "Cryptomator"; + + private final Optional wallet; + + public KDEWalletKeychainAccess() { + this.wallet = ConnectedWallet.connect(); + } + + @Override + public boolean isSupported() { + return wallet.map(ConnectedWallet::isSupported).orElse(false); + } + + @Override + public boolean isLocked() { + return wallet.map(ConnectedWallet::isLocked).orElse(false); + } + + @Override + public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); + wallet.get().storePassphrase(key, passphrase); + } + + @Override + public char[] loadPassphrase(String key) throws KeychainAccessException { + CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); + return wallet.get().loadPassphrase(key); + } + + @Override + public void deletePassphrase(String key) throws KeychainAccessException { + CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); + wallet.get().deletePassphrase(key); + } + + @Override + public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + CheckUtil.checkState(wallet.isPresent(), "Keychain not supported."); + wallet.get().changePassphrase(key, passphrase); + } + + private static class ConnectedWallet { + + private final KDEWallet wallet; + private int handle = -1; + + public ConnectedWallet(DBusConnection connection) { + this.wallet = new KDEWallet(connection); + } + + static Optional connect() { + try { + return Optional.of(new ConnectedWallet(getNewConnection())); + } catch (DBusException e) { + LOG.warn("Connecting to D-Bus failed.", e); + return Optional.empty(); + } + } + + private static DBusConnection getNewConnection() throws DBusException { + try { + return DBusConnectionBuilder.forSessionBus().withShared(false).build(); + } catch (DBusConnectionException | DBusExecutionException de) { + LOG.warn("Connecting to SESSION bus failed.", de); + LOG.warn("Falling back to SYSTEM DBus"); + return DBusConnectionBuilder.forSystemBus().build(); + } + } + + public boolean isSupported() { + try { + return wallet.isEnabled(); + } catch (RuntimeException e) { + LOG.warn("Failed to check if KDE Wallet is available.", e); + return false; + } + } + + public boolean isLocked() { + try { + return !wallet.isOpen(Static.DEFAULT_WALLET); + } catch (RuntimeException e) { + LOG.warn("Failed to check whether KDE Wallet is open, therefore considering it closed.", e); + return true; + } + } + + public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + try { + if (walletIsOpen() && + !(wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1) + && wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) { + LOG.debug("Passphrase successfully stored."); + } else { + LOG.debug("Passphrase was not stored."); + } + } catch (RuntimeException e) { + throw new KeychainAccessException("Storing the passphrase failed.", e); + } + } + + public char[] loadPassphrase(String key) throws KeychainAccessException { + String password = ""; + try { + if (walletIsOpen()) { + password = wallet.readPassword(handle, FOLDER_NAME, key, APP_NAME); + LOG.debug("loadPassphrase: wallet is open."); + } else { + LOG.debug("loadPassphrase: wallet is closed."); + } + return (password.isEmpty()) ? null : password.toCharArray(); + } catch (RuntimeException e) { + throw new KeychainAccessException("Loading the passphrase failed.", e); + } + } + + public void deletePassphrase(String key) throws KeychainAccessException { + try { + if (walletIsOpen() + && wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) + && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 + && wallet.removeEntry(handle, FOLDER_NAME, key, APP_NAME) == 0) { + LOG.debug("Passphrase successfully deleted."); + } else { + LOG.debug("Passphrase was not deleted."); + } + } catch (RuntimeException e) { + throw new KeychainAccessException("Deleting the passphrase failed.", e); + } + } + + public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + try { + if (walletIsOpen() + && wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) + && wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 + && wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) { + LOG.debug("Passphrase successfully changed."); + } else { + LOG.debug("Passphrase could not be changed."); + } + } catch (RuntimeException e) { + throw new KeychainAccessException("Changing the passphrase failed.", e); + } + } + + private boolean walletIsOpen() throws KeychainAccessException { + try { + if (wallet.isOpen(Static.DEFAULT_WALLET)) { + // This is needed due to KeechainManager loading the passphase directly + if (handle == -1) handle = wallet.open(Static.DEFAULT_WALLET, 0, APP_NAME); + return true; + } + wallet.openAsync(Static.DEFAULT_WALLET, 0, APP_NAME, false); + wallet.getSignalHandler().await(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5, () -> null); + handle = wallet.getSignalHandler().getLastHandledSignal(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5).handle; + LOG.debug("Wallet successfully initialized."); + return handle != -1; + } catch (RuntimeException e) { + throw new KeychainAccessException("Asynchronous opening the wallet failed.", e); + } + } + + + } +} + diff --git a/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider index 6de442f..3b9354d 100644 --- a/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider +++ b/src/main/resources/META-INF/services/org.cryptomator.integrations.keychain.KeychainAccessProvider @@ -1 +1,3 @@ -org.cryptomator.linux.keychain.SecretServiceKeychainAccess \ No newline at end of file +org.cryptomator.linux.keychain.SecretServiceKeychainAccess +org.cryptomator.linux.keychain.KDEWalletKeychainAccess +org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess \ No newline at end of file diff --git a/src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java new file mode 100644 index 0000000..ebf1df2 --- /dev/null +++ b/src/test/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccessTest.java @@ -0,0 +1,90 @@ +package org.cryptomator.linux.keychain; + +import org.cryptomator.integrations.keychain.KeychainAccessException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for GNOME keyring access via DBUS. + */ +@EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".*") +public class GnomeKeyringKeychainAccessTest { + + private static boolean isInstalled; + + @BeforeAll + public static void checkSystemAndSetup() throws IOException { + ProcessBuilder dbusSend = new ProcessBuilder("dbus-send", "--print-reply", "--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListNames"); + ProcessBuilder grep = new ProcessBuilder("grep", "-q", "org.gnome.keyring"); + try { + Process end = ProcessBuilder.startPipeline(List.of(dbusSend, grep)).get(1); + if (end.waitFor(1000, TimeUnit.MILLISECONDS)) { + isInstalled = end.exitValue() == 0; + } else { + isInstalled = false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Test + public void testIsSupported() { + var gnomeKeyring = new GnomeKeyringKeychainAccess(); + Assertions.assertEquals(isInstalled, gnomeKeyring.isSupported()); + } + + @Nested + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @EnabledIf("gnomeKeyringAvailableAndUnlocked") + class FunctionalTests { + + static final String KEY_ID = "cryptomator-test-" + UUID.randomUUID(); + final GnomeKeyringKeychainAccess gnomeKeyring = new GnomeKeyringKeychainAccess(); + + @Test + @Order(1) + public void testStore() throws KeychainAccessException { + gnomeKeyring.storePassphrase(KEY_ID, "cryptomator-test", "p0ssw0rd"); + } + + @Test + @Order(2) + public void testLoad() throws KeychainAccessException { + var passphrase = gnomeKeyring.loadPassphrase(KEY_ID); + Assertions.assertNotNull(passphrase); + Assertions.assertEquals("p0ssw0rd", String.copyValueOf(passphrase)); + } + + @Test + @Order(3) + public void testDelete() throws KeychainAccessException { + gnomeKeyring.deletePassphrase(KEY_ID); + } + + @Test + @Order(4) + public void testLoadNotExisting() throws KeychainAccessException { + var result = gnomeKeyring.loadPassphrase(KEY_ID); + Assertions.assertNull(result); + } + + public static boolean gnomeKeyringAvailableAndUnlocked() { + var secretServiceKeychain = new GnomeKeyringKeychainAccess(); + return secretServiceKeychain.isSupported() && !secretServiceKeychain.isLocked(); + } + } + +} diff --git a/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java b/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java new file mode 100644 index 0000000..f845dde --- /dev/null +++ b/src/test/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccessTest.java @@ -0,0 +1,45 @@ +package org.cryptomator.linux.keychain; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for KWallet access via DBUS. + */ +@EnabledOnOs(OS.LINUX) +@EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".*") +public class KDEWalletKeychainAccessTest { + + private static boolean isInstalled; + + @BeforeAll + public static void checkSystemAndSetup() throws IOException { + ProcessBuilder dbusSend = new ProcessBuilder("dbus-send", "--print-reply", "--dest=org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus.ListActivatableNames"); + ProcessBuilder grep = new ProcessBuilder("grep", "-q", "org.kde.kwallet"); + try { + Process end = ProcessBuilder.startPipeline(List.of(dbusSend, grep)).get(1); + if (end.waitFor(1000, TimeUnit.MILLISECONDS)) { + isInstalled = end.exitValue() == 0; + } else { + isInstalled = false; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + + @Test + public void testIsSupported() { + KDEWalletKeychainAccess keychainAccess = new KDEWalletKeychainAccess(); + Assertions.assertEquals(isInstalled, keychainAccess.isSupported()); + } +} From 1c54f97c558f933d3d0aa3f11e896a4e7240bb33 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Mon, 27 Oct 2025 19:13:26 +0100 Subject: [PATCH 8/9] Remove added line by IDE --- src/main/java/module-info.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index facc5e2..7384e5d 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -21,7 +21,6 @@ requires de.swiesend.secretservice; requires org.purejava.secret; requires java.xml; - requires org.cryptomator.integrations.linux; provides AutoStartProvider with FreedesktopAutoStartService; provides KeychainAccessProvider with SecretServiceKeychainAccess, GnomeKeyringKeychainAccess, KDEWalletKeychainAccess; From 17a4f69a470accc7a9a75e66a3f1fb8a6e424de2 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Tue, 28 Oct 2025 18:21:17 +0100 Subject: [PATCH 9/9] Keep migrated items Give it a higher priority Use latest Secret Service library --- pom.xml | 2 +- .../linux/keychain/SecretServiceKeychainAccess.java | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 5e81210..cd491c3 100644 --- a/pom.xml +++ b/pom.xml @@ -43,7 +43,7 @@ 1.7.0 2.0.1-alpha 1.4.0 - 1.0.0 + 1.0.1 2.0.17 1.4.2 diff --git a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java index 62e858c..927b83f 100644 --- a/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java +++ b/src/main/java/org/cryptomator/linux/keychain/SecretServiceKeychainAccess.java @@ -20,10 +20,11 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -@Priority(900) +@Priority(1100) @OperatingSystem(OperatingSystem.Value.LINUX) @DisplayName("Secret Service") public class SecretServiceKeychainAccess implements KeychainAccessProvider { @@ -176,15 +177,21 @@ private void migrateKDEWalletEntries() { session.getService().ensureUnlocked(i); var item = new Item(i); var secret = item.getSecret(session.getSession()); + Map newAttribs = new HashMap<>(attribs.value()); + newAttribs.put("server", "Cryptomator - already migrated"); + var label = item.getLabel().value(); + var itemProps = Item.createProperties(label, newAttribs); + var replace = collection.createItem(itemProps, secret, true); + assert replace.isSuccess() : "Replacing migrated item failed"; + item.delete(); try { storePassphrase(attribs.value().get("user"), "Cryptomator", new String(session.decrypt(secret))); + LOG.info("Successfully migrated password for vault {}", attribs.value().get("user")); } catch (KeychainAccessException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { LOG.error("Migrating entry {} for vault {} failed", i.getPath(), attribs.value().get("user")); } - item.delete(); - LOG.info("Successfully migrated password for vault {}", attribs.value().get("user")); } } }