Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<api.version>1.7.0</api.version>
<secret-service.version>2.0.1-alpha</secret-service.version>
<kdewallet.version>1.4.0</kdewallet.version>
<secret-service-02.version>1.0.0</secret-service-02.version>
<slf4j.version>2.0.17</slf4j.version>
<appindicator.version>1.4.2</appindicator.version>

Expand Down Expand Up @@ -82,6 +83,11 @@
<artifactId>kdewallet</artifactId>
<version>${kdewallet.version}</version>
</dependency>
<dependency>
<groupId>org.purejava</groupId>
<artifactId>secret-service</artifactId>
<version>${secret-service-02.version}</version>
</dependency>
<!-- Java bindings for appindicator -->
<dependency>
<groupId>org.purejava</groupId>
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,10 +19,11 @@
requires org.purejava.appindicator;
requires org.purejava.kwallet;
requires de.swiesend.secretservice;
requires org.purejava.secret;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 is org.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:

#!/bin/bash
# Description: Extract and verify the module name from the secret-service JAR

# Find the secret-service JAR in Maven local repository or target
secret_jar=$(fd -e jar -g '*secret-service*.jar' ~/.m2/repository 2>/dev/null | head -1)

if [ -z "$secret_jar" ]; then
  echo "Secret service JAR not found in local Maven repository."
  echo "Attempting to check Maven Central for module info..."
  # Alternative: check Maven Central metadata
  curl -s "https://repo1.maven.org/maven2/org/purejava/secret-service/maven-metadata.xml" | grep -o '<latest>[^<]*</latest>' || echo "Could not retrieve version info"
else
  echo "Found JAR: $secret_jar"
  echo "Extracting module-info..."
  unzip -p "$secret_jar" module-info.class > /tmp/module-info.class 2>/dev/null
  if [ -f /tmp/module-info.class ]; then
    javap /tmp/module-info.class | grep "^module" || echo "Module info not found or not readable"
  else
    echo "Could not extract module-info.class from JAR"
  fi
fi

Length of output: 131


🌐 Web query:

org.purejava secret-service module name Java

💡 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:

org.purejava secret-service GitHub Java library

💡 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:

  • purejava GitHub org (repositories list). [1]
  • org.purejava: kdewallet (Maven / project page). [2]
  • org.purejava: keepassxc-proxy-access (Maven / project page). [3]

🌐 Web query:

Maven Central org.purejava secret-service 1.0.0 jar module

💡 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:

  • de.swiesend:secret-service (pre‑release/RC versions shown). [3]
  • other "secret"/"secret-core" artifacts under com.github.houbb (unrelated groupId/artifactId). [4]

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:

  1. Maven Central org.purejava namespace. [https://central.sonatype.com/namespace/org.purejava] [1]
  2. Search results for org.purejava artifacts on Central (keepassxc-proxy-access, winsparkle-java). [https://central.sonatype.com/artifact/org.purejava/keepassxc-proxy-access] [2]
  3. mvnrepository / listings showing secret-service under other groups (de.swiesend / similar). [https://mvnrepository.com/artifact/com.github.javakeyring/java-keyring/1.0.0] [3]
  4. JarCasting pages for com.github.houbb:secret and secret-core. [https://jarcasting.com/artifacts/com.github.houbb/secret-core/] [4]

🏁 Script executed:

#!/bin/bash
# Find pom.xml and search for secret-service dependency
fd -name pom.xml -type f | head -5

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Find pom.xml using find command
find . -name pom.xml -type f | head -5

Length of output: 9


🏁 Script executed:

#!/bin/bash
# Display pom.xml to find the dependency declaration
cat pom.xml

Length of output: 8613


🏁 Script executed:

#!/bin/bash
# Display module-info.java to see all requires directives
cat src/main/java/module-info.java

Length of output: 1745


🌐 Web query:

org.purejava secret-service Maven Central repository versions

💡 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:

  • purejava/secret-service GitHub (shows dependency snippet and "No packages published"). [1]

The dependency org.purejava:secret-service:1.0.0 is not published on Maven Central and will cause build failures.

The pom.xml declares org.purejava:secret-service version 1.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.

requires java.xml;

provides AutoStartProvider with FreedesktopAutoStartService;
provides KeychainAccessProvider with GnomeKeyringKeychainAccess, KDEWalletKeychainAccess;
provides KeychainAccessProvider with SecretServiceKeychainAccess, GnomeKeyringKeychainAccess, KDEWalletKeychainAccess;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Provider registration looks correct. Verify deprecation of old implementations.

The provider list is correctly updated with SecretServiceKeychainAccess first, ensuring the new unified implementation is preferred. Retaining the old providers supports backwards compatibility as suggested by the maintainer.

However, please verify that GnomeKeyringKeychainAccess and KDEWalletKeychainAccess are marked with @Deprecated annotations in their class definitions, as the maintainer recommended keeping them deprecated before eventual removal in a future release.

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 java

Length 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.java

Length of output: 637


Add @Deprecated annotations to old keychain implementations.

The old GnomeKeyringKeychainAccess and KDEWalletKeychainAccess implementations are not marked with @Deprecated annotations. Both classes require @Deprecated to signal that they are superseded by SecretServiceKeychainAccess and will be removed in a future release.

Add @Deprecated annotation to:

  • src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java (before the class declaration)
  • src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java (before the class declaration)
🤖 Prompt for AI Agents
In src/main/java/org/cryptomator/linux/keychain/GnomeKeyringKeychainAccess.java
(at the class declaration) and
src/main/java/org/cryptomator/linux/keychain/KDEWalletKeychainAccess.java (at
the class declaration), add the @Deprecated annotation immediately above each
class declaration to mark them as superseded by SecretServiceKeychainAccess and
slated for removal; ensure the annotation is imported if necessary and place it
on the class-level (before any javadoc or modifiers) so the compiler and IDEs
surface deprecation warnings.

provides RevealPathService with DBusSendRevealPathService;
provides TrayMenuController with AppindicatorTrayMenuController;
provides QuickAccessService with NautilusBookmarks, DolphinPlaces;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,4 @@ private boolean walletIsOpen() throws KeychainAccessException {

}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
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.List;
import java.util.Map;

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

@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());
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think migration of such data should never delete the original. Instead, is there an option to mark it as migrated, so it won't migrate twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do the passwords stored and used by KDEWalletKeychainAccess have a need, when we remove KDEWalletKeychainAccess with a future PR? Nevertheless, I changed the behavior as suggested with 17a4f69.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is, how it looks like after migration. The label "Cryptomator" will get changed on a password update/change to the vault's display name, but this information is not yet available on migrating items.
Bildschirmfoto_20251029_054433

LOG.info("Successfully migrated password for vault {}", 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
Expand Up @@ -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();
}
}

}