-
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
ca6b4d8
857c812
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 |
|---|---|---|
|
|
@@ -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"; | ||||||||||||||||||
|
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. Replace assertion with proper error handling. Using 🔎 Proposed fix var itemProps = Item.createProperties(label, newAttribs);
var replace = collection.createItem(itemProps, secret, true);
- assert replace.isSuccess() : "Replacing migrated item failed";
+ if (!replace.isSuccess()) {
+ LOG.error("Replacing migrated item {} failed: {}", i.getPath(), replace.error());
+ continue;
+ }
item.delete();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
| 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(); | ||
| } | ||
| } | ||
|
|
||
| } |
Uh oh!
There was an error while loading. Please reload this page.