-
Notifications
You must be signed in to change notification settings - Fork 5
New secret service #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
New secret service #125
Changes from all commits
a8a57c2
13e3132
5163d8b
9f227d0
c22f813
161ea15
b9c6eb1
1c54f97
17a4f69
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,9 @@ | |
| 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.KDEWalletKeychainAccess; | ||
| import org.cryptomator.linux.keychain.SecretServiceKeychainAccess; | ||
| import org.cryptomator.linux.quickaccess.DolphinPlaces; | ||
| import org.cryptomator.linux.quickaccess.NautilusBookmarks; | ||
| import org.cryptomator.linux.revealpath.DBusSendRevealPathService; | ||
|
|
@@ -18,10 +19,11 @@ | |
| 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, GnomeKeyringKeychainAccess, KDEWalletKeychainAccess; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainProvider registration looks correct. Verify deprecation of old implementations. The provider list is correctly updated with However, please verify that Run the following script to check if the old implementations are marked as deprecated: 🏁 Script executed: #!/bin/bash
# Description: Check if old keychain implementations are marked with @Deprecated
echo "=== Checking GnomeKeyringKeychainAccess ==="
rg -A 2 -B 2 "class GnomeKeyringKeychainAccess" --type java
echo -e "\n=== Checking KDEWalletKeychainAccess ==="
rg -A 2 -B 2 "class KDEWalletKeychainAccess" --type javaLength of output: 2448 [/run_scripts] 🏁 Script executed: #!/bin/bash
# Description: Explicitly check for @Deprecated annotation in old keychain implementations
echo "=== Searching for @Deprecated in GnomeKeyringKeychainAccess.java ==="
rg "@Deprecated" src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java
echo -e "\n=== Searching for @Deprecated in KDEWalletKeychainAccess.java ==="
rg "@Deprecated" src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java
echo -e "\n=== Full class declaration for GnomeKeyringKeychainAccess ==="
rg -B 5 "public class GnomeKeyringKeychainAccess" src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java
echo -e "\n=== Full class declaration for KDEWalletKeychainAccess ==="
rg -B 5 "public class KDEWalletKeychainAccess" src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.javaLength of output: 637 Add The old Add
🤖 Prompt for AI Agents |
||
| provides RevealPathService with DBusSendRevealPathService; | ||
| provides TrayMenuController with AppindicatorTrayMenuController; | ||
| provides QuickAccessService with NautilusBookmarks, DolphinPlaces; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -193,3 +193,4 @@ private boolean walletIsOpen() throws KeychainAccessException { | |
|
|
||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| 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 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.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| @Priority(1100) | ||
| @OperatingSystem(OperatingSystem.Value.LINUX) | ||
| @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())); | ||
| 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())); | ||
|
|
||
| migrateKDEWalletEntries(); | ||
| } | ||
|
|
||
| @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<DBusPath> 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); | ||
| 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); | ||
| } | ||
| } | ||
purejava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| @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(); | ||
| session.getService().ensureUnlocked(path); | ||
| 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(); | ||
| session.getService().ensureUnlocked(path); | ||
| 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()) { | ||
| 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); | ||
| 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<String, String> 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()); | ||
| Map<String, String> 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")); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| org.cryptomator.linux.keychain.SecretServiceKeychainAccess | ||
| org.cryptomator.linux.keychain.KDEWalletKeychainAccess | ||
| org.cryptomator.linux.keychain.GnomeKeyringKeychainAccess |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,4 +87,4 @@ public static boolean gnomeKeyringAvailableAndUnlocked() { | |
| } | ||
| } | ||
|
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| 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 Secret Service access via Dbus. | ||
| */ | ||
| @EnabledIfEnvironmentVariable(named = "DISPLAY", matches = ".*") | ||
| 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.freedesktop.secrets"); | ||
| 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 service = new SecretServiceKeychainAccess(); | ||
| Assertions.assertEquals(isInstalled, service.isSupported()); | ||
| } | ||
|
|
||
| @Nested | ||
| @TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||
| @EnabledIf("serviceAvailableAndUnlocked") | ||
| class FunctionalTests { | ||
|
|
||
| static final String KEY_ID = "cryptomator-test-" + UUID.randomUUID(); | ||
| final static SecretServiceKeychainAccess KEYRING = new SecretServiceKeychainAccess(); | ||
|
|
||
| @Test | ||
| @Order(1) | ||
| public void testStore() throws KeychainAccessException { | ||
| 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); | ||
| Assertions.assertNotNull(passphrase); | ||
| Assertions.assertEquals("p0ssw0rd", String.copyValueOf(passphrase)); | ||
| } | ||
|
|
||
| @Test | ||
| @Order(3) | ||
| public void testDelete() throws KeychainAccessException { | ||
| KEYRING.deletePassphrase(KEY_ID); | ||
| } | ||
|
|
||
| @Test | ||
| @Order(4) | ||
| public void testLoadNotExisting() throws KeychainAccessException { | ||
| var result = KEYRING.loadPassphrase(KEY_ID); | ||
| Assertions.assertNull(result); | ||
| } | ||
|
|
||
| public static boolean serviceAvailableAndUnlocked() { | ||
| var service = new SecretServiceKeychainAccess(); | ||
| return service.isSupported() && !service.isLocked(); | ||
| } | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify the module name for the secret-service dependency.
The module name is declared as
org.purejava.secret, but according to the AI summary, the Maven artifact isorg.purejava:secret-service. Module names don't always match Maven artifact names, but this should be verified to ensure module resolution succeeds at compile time.Run the following script to verify the module name exported by the secret-service JAR:
🏁 Script executed:
Length of output: 131
🌐 Web query:
💡 Result:
Do you mean the Java module name (module-info / Automatic-Module-Name) for an org.purejava "secret-service" artifact, or the Maven artifactId/groupId to add as a dependency? If you mean one of those, tell me which and I’ll look it up.
🌐 Web query:
💡 Result:
I couldn't find a GitHub repository named "org.purejava/secret-service". The purejava (org.purejava) GitHub organization (owner: Ralph Plawetzki) hosts several Java libraries related to secrets/security instead — e.g. kdewallet and keepassxc-proxy-access. [1][2][3]
Sources:
🌐 Web query:
💡 Result:
I couldn't find org.purejava:secret-service:1.0.0 on Maven Central. I searched Maven Central/sonatype and public Maven indexes — there is no artifact with groupId org.purejava and artifactId secret-service at version 1.0.0. [1][2]
Related hits (different groupIds) you may want to check:
If you want, tell me whether you mean one of those (de.swiesend or com.github.houbb), or provide a pom.xml/URL and I’ll look up the exact Maven coordinates and the available module/jar files.
Sources:
🏁 Script executed:
Length of output: 230
🏁 Script executed:
Length of output: 9
🏁 Script executed:
Length of output: 8613
🏁 Script executed:
Length of output: 1745
🌐 Web query:
💡 Result:
I couldn't find any versions of org.purejava:secret-service on Maven Central — the project's GitHub README shows the intended Maven/Gradle coordinates (version 1.0.0) but the repository shows "No packages published" (i.e. not on Maven Central). [1]
Sources:
The dependency
org.purejava:secret-service:1.0.0is not published on Maven Central and will cause build failures.The pom.xml declares
org.purejava:secret-serviceversion1.0.0, but the project's GitHub shows the intended Maven coordinates but repository shows "No packages published". The build will fail during dependency resolution. Either use a different version if available, or ensure this artifact is published to the repository before building.