From 5d85e9bde5cb8ac16dc978c602bf488a957a38dd Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 21 Oct 2021 14:47:38 +0200 Subject: [PATCH 01/74] Add versions-maven-plugin for dependency updates --- pom.xml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pom.xml b/pom.xml index 83ba256..f450864 100644 --- a/pom.xml +++ b/pom.xml @@ -165,6 +165,7 @@ + @@ -238,6 +239,11 @@ jacoco-maven-plugin 0.8.10 + + org.codehaus.mojo + versions-maven-plugin + 2.8.1 + @@ -328,6 +334,17 @@ + + org.codehaus.mojo + versions-maven-plugin + + false + false + true + + true + + From cf74249add140ae70a781480956d14e5c1f9e0ef Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 20 Feb 2022 20:13:00 +0100 Subject: [PATCH 02/74] Require JDK17 and update dbus-java to version `4.0.0` and --- pom.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f450864..dcf743b 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,8 @@ ${hkdf.version} - + + com.github.hypfvieh dbus-java-core From 451ff3eb3e05e28d038b0b6ff8a5542fbba5ed96 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 6 Mar 2022 13:42:27 +0100 Subject: [PATCH 03/74] Add Optional<> --- pom.xml | 6 ++ .../handlers/MessageHandler.java | 59 +++++++++++-------- .../secretservice/handlers/Messaging.java | 13 ++-- .../secretservice/handlers/SignalHandler.java | 5 +- 4 files changed, 51 insertions(+), 32 deletions(-) diff --git a/pom.xml b/pom.xml index dcf743b..af7f0eb 100644 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,12 @@ test + + org.apache.commons + commons-lang3 + 3.12.0 + + diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 1e46ac9..2e39495 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -1,5 +1,6 @@ package de.swiesend.secretservice.handlers; +import org.apache.commons.lang3.ArrayUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.messages.MethodCall; @@ -9,6 +10,7 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.Optional; import static de.swiesend.secretservice.Static.DBus.MAX_DELAY_MILLIS; @@ -22,7 +24,7 @@ public MessageHandler(DBusConnection connection) { this.connection = connection; } - public Object[] send(String service, String path, String iface, String method, String signature, Object... args) { + public Optional send(String service, String path, String iface, String method, String signature, Object... args) { try { org.freedesktop.dbus.messages.Message message = new MethodCall( service, @@ -46,53 +48,60 @@ public Object[] send(String service, String path, String iface, String method, S switch (error) { case "org.freedesktop.Secret.Error.NoSession": case "org.freedesktop.Secret.Error.NoSuchObject": - log.warn(error + ": " + parameters[0]); - return null; + if (ArrayUtils.isEmpty(parameters)) { + log.warn(error); + } else { + log.warn(error + ": " + parameters[0]); + } + return Optional.empty(); case "org.gnome.keyring.Error.Denied": case "org.freedesktop.Secret.Error.IsLocked": - log.info(error + ": " + parameters[0]); - return null; + if (ArrayUtils.isEmpty(parameters)) { + log.info(error); + } else { + log.info(error + ": " + parameters[0]); + } + return Optional.empty(); case "org.freedesktop.DBus.Error.NoReply": case "org.freedesktop.DBus.Error.UnknownMethod": case "org.freedesktop.DBus.Error.ServiceUnknown": case "org.freedesktop.dbus.exceptions.NotConnected": case "org.freedesktop.DBus.Local.Disconnected": case "org.freedesktop.dbus.exceptions.FatalDBusException": - log.debug(error); - return null; + if (log.isDebugEnabled()) log.debug(error); + return Optional.empty(); default: - throw new DBusException(error); + log.error(error); + return Optional.empty(); } } - return parameters; + return Optional.ofNullable(parameters); } catch (DBusException e) { log.error("Unexpected D-Bus response:", e); } - return null; + return Optional.empty(); } - public Variant getProperty(String service, String path, String iface, String property) { - Object[] response = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, + public Optional getProperty(String service, String path, String iface, String property) { + Optional maybeResponse = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, "Get", "ss", iface, property); - if (response == null) return null; - return (Variant) response[0]; + if (!maybeResponse.isPresent()) return Optional.empty(); + Object[] response = maybeResponse.get(); + return ArrayUtils.isEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); } - public Variant getAllProperties(String service, String path, String iface) { - Object[] response = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, + public Optional getAllProperties(String service, String path, String iface) { + Optional maybeResponse = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, "GetAll", "ss", iface); - if (response == null) return null; - return (Variant) response[0]; + if (!maybeResponse.isPresent()) return Optional.empty(); + Object[] response = maybeResponse.get(); + return ArrayUtils.isEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); } - public void setProperty(String service, String path, String iface, String property, Variant value) { - send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, - "Set", "ssv", iface, property, value); - } - - public DBusConnection getConnection() { - return connection; + public boolean setProperty(String service, String path, String iface, String property, Variant value) { + Optional maybeResponse = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, "Set", "ssv", iface, property, value); + return maybeResponse.isPresent(); } } diff --git a/src/main/java/de/swiesend/secretservice/handlers/Messaging.java b/src/main/java/de/swiesend/secretservice/handlers/Messaging.java index 2b316d0..1cf5fb9 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/Messaging.java +++ b/src/main/java/de/swiesend/secretservice/handlers/Messaging.java @@ -7,6 +7,7 @@ import de.swiesend.secretservice.Static; import java.util.List; +import java.util.Optional; public abstract class Messaging { @@ -29,24 +30,24 @@ public Messaging(DBusConnection connection, List> si this.interfaceName = interfaceName; } - protected Object[] send(String method) { + protected Optional send(String method) { return msg.send(serviceName, objectPath, interfaceName, method, ""); } - protected Object[] send(String method, String signature, Object... arguments) { + protected Optional send(String method, String signature, Object... arguments) { return msg.send(serviceName, objectPath, interfaceName, method, signature, arguments); } - protected Variant getProperty(String property) { + protected Optional getProperty(String property) { return msg.getProperty(serviceName, objectPath, interfaceName, property); } - protected Variant getAllProperties() { + protected Optional getAllProperties() { return msg.getAllProperties(serviceName, objectPath, interfaceName); } - protected void setProperty(String property, Variant value) { - msg.setProperty(serviceName, objectPath, interfaceName, property, value); + protected boolean setProperty(String property, Variant value) { + return msg.setProperty(serviceName, objectPath, interfaceName, property, value); } public String getServiceName() { diff --git a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java index f402c8d..93055ce 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java @@ -46,8 +46,11 @@ public void connect(DBusConnection connection, List> } } } catch (DBusException e) { - log.error("Could not connect to the D-Bus.", e); + log.error("Could not connect to the D-Bus: ", e); + } catch (ClassCastException e) { + log.error("Could not cast a signal: ", e); } + } } From ed834ff85fd441edd3a858313d322eef1c240d72 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 6 Mar 2022 13:49:37 +0100 Subject: [PATCH 04/74] Add better error handling --- .../de/swiesend/secretservice/handlers/MessageHandler.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 2e39495..17e9d37 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -71,14 +71,16 @@ public Optional send(String service, String path, String iface, String if (log.isDebugEnabled()) log.debug(error); return Optional.empty(); default: - log.error(error); + log.error("Unexpected org.freedesktop.dbus.errors.Error: ", error); return Optional.empty(); } } return Optional.ofNullable(parameters); } catch (DBusException e) { - log.error("Unexpected D-Bus response:", e); + log.error("Unexpected D-Bus response: ", e); + } catch (RuntimeException e) { + log.error("Unexpected: ", e); } return Optional.empty(); } From 915445d9351fede3ac59424de01de9fd0ee98877 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 2 Apr 2022 15:06:15 +0200 Subject: [PATCH 05/74] Let `MessageHandler` return `Optional`s --- .../de/swiesend/secretservice/Collection.java | 72 +++--- .../java/de/swiesend/secretservice/Item.java | 77 ++++--- .../de/swiesend/secretservice/Prompt.java | 17 +- .../de/swiesend/secretservice/Service.java | 102 +++++---- .../de/swiesend/secretservice/Session.java | 4 +- .../de/swiesend/secretservice/Static.java | 31 ++- .../secretservice/TransportEncryption.java | 7 +- ...ternalUnsupportedGuiltRiddenInterface.java | 31 ++- ...ternalUnsupportedGuiltRiddenInterface.java | 9 +- .../handlers/MessageHandler.java | 28 ++- .../secretservice/interfaces/Collection.java | 207 +++++++++--------- .../secretservice/interfaces/Item.java | 30 +-- .../secretservice/interfaces/Prompt.java | 9 +- .../secretservice/interfaces/Service.java | 67 +++--- .../secretservice/interfaces/Session.java | 4 +- .../simple/SimpleCollection.java | 50 +++-- .../integration/CollectionTest.java | 36 +-- .../integration/IntegrationTest.java | 8 +- .../secretservice/integration/ItemTest.java | 32 +-- .../secretservice/integration/PromptTest.java | 6 +- .../integration/ServiceTest.java | 79 ++++--- .../secretservice/integration/StaticTest.java | 26 ++- ...alUnsupportedGuiltRiddenInterfaceTest.java | 8 +- .../integration/test/Context.java | 13 +- 24 files changed, 502 insertions(+), 451 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/Collection.java b/src/main/java/de/swiesend/secretservice/Collection.java index 8afefa5..338f95e 100644 --- a/src/main/java/de/swiesend/secretservice/Collection.java +++ b/src/main/java/de/swiesend/secretservice/Collection.java @@ -5,16 +5,16 @@ import org.freedesktop.dbus.messages.DBusSignal; import org.freedesktop.dbus.types.UInt64; import org.freedesktop.dbus.types.Variant; +import de.swiesend.secretservice.Static.Utils; import de.swiesend.secretservice.handlers.Messaging; import java.util.*; public class Collection extends Messaging implements de.swiesend.secretservice.interfaces.Collection { - private String id; - public static final List> signals = Arrays.asList( ItemCreated.class, ItemChanged.class, ItemDeleted.class); + private String id; public Collection(DBusPath path, Service service) { super(service.getConnection(), signals, @@ -70,71 +70,69 @@ public Collection(String id, Service service) { */ public static Map createProperties(String label) { HashMap properties = new HashMap(); - properties.put(LABEL, new Variant(label)); + properties.put(LABEL, new Variant<>(label)); return properties; } @Override - public ObjectPath delete() { - Object[] response = send("Delete", ""); - if (response == null) return null; - ObjectPath prompt = (ObjectPath) response[0]; + public Optional delete() { + Optional prompt = send("Delete", "").flatMap(response -> + Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.of((ObjectPath) response[0]) + ); return prompt; } @Override - public List searchItems(Map attributes) { - Object[] response = send("SearchItems", "a{ss}", attributes); - if (response == null) return null; - return (List) response[0]; + public Optional> searchItems(Map attributes) { + return send("SearchItems", "a{ss}", attributes).flatMap(response -> + Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.of((List) response[0]) + ); } @Override - public Pair createItem(Map properties, Secret secret, - boolean replace) { - Object[] response = send("CreateItem", "a{sv}(oayays)b", properties, secret, replace); - if (response == null) return null; - return new Pair(response[0], response[1]); + public Optional> createItem(Map properties, Secret secret, boolean replace) { + return send("CreateItem", "a{sv}(oayays)b", properties, secret, replace).flatMap(response -> + (Utils.isNullOrEmpty(response) || response.length != 2) ? + Optional.empty() : + Optional.of(new Pair((ObjectPath) response[0], (ObjectPath) response[1]))); } @Override - public List getItems() { - Variant response = getProperty("Items"); - if (response == null) return null; - return (ArrayList) response.getValue(); + public Optional> getItems() { + return getProperty("Items").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((List) variant.getValue()) + ); } @Override - public String getLabel() { - Variant response = getProperty("Label"); - if (response == null) return null; - return (String) response.getValue(); + public Optional getLabel() { + return getProperty("Label").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((String) variant.getValue()) + ); } @Override - public void setLabel(String label) { - setProperty("Label", new Variant(label)); + public boolean setLabel(String label) { + return setProperty("Label", new Variant(label)); } @Override public boolean isLocked() { - Variant response = getProperty("Locked"); - if (response == null) return true; - return (boolean) response.getValue(); + Optional response = getProperty("Locked").map(variant -> + variant == null ? false : (boolean) variant.getValue()); + return response.isPresent() ? response.get() : false; } @Override - public UInt64 created() { - Variant response = getProperty("Created"); - if (response == null) return null; - return (UInt64) response.getValue(); + public Optional created() { + return getProperty("Created").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((UInt64) variant.getValue())); } @Override - public UInt64 modified() { - Variant response = getProperty("Modified"); - if (response == null) return null; - return (UInt64) response.getValue(); + public Optional modified() { + return getProperty("Modified").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((UInt64) variant.getValue())); } @Override diff --git a/src/main/java/de/swiesend/secretservice/Item.java b/src/main/java/de/swiesend/secretservice/Item.java index 23be93a..26d04bc 100644 --- a/src/main/java/de/swiesend/secretservice/Item.java +++ b/src/main/java/de/swiesend/secretservice/Item.java @@ -82,17 +82,21 @@ public static Map createProperties(String label, Map delete() { + Object[] response = send("Delete", "").orElse(null); + if (Static.Utils.isNullOrEmpty(response)) return Optional.empty(); + try { + ObjectPath prompt = (ObjectPath) response[0]; + return Optional.of(prompt); + } catch (ClassCastException e) { + return Optional.empty(); + } } @Override - public Secret getSecret(ObjectPath session) { - Object[] response = send("GetSecret", "o", session); - if (response == null) return null; + public Optional getSecret(ObjectPath session) { + Object[] response = send("GetSecret", "o", session).orElse(null); + if (Static.Utils.isNullOrEmpty(response)) return Optional.empty(); try { Object[] inner = (Object[]) response[0]; @@ -110,67 +114,62 @@ public Secret getSecret(ObjectPath session) { secret = new Secret(session_path, parameters, value, contentType); } - return secret; + return Optional.of(secret); } catch (ClassCastException | IndexOutOfBoundsException e) { - return null; + return Optional.empty(); } } @Override - public void setSecret(Secret secret) { - send("SetSecret", "(oayays)", secret); + public boolean setSecret(Secret secret) { + return send("SetSecret", "(oayays)", secret).isPresent(); } @Override public boolean isLocked() { - Variant response = getProperty("Locked"); - if (response == null) return true; - return (boolean) response.getValue(); + Optional response = getProperty("Locked").map(variant -> + variant == null ? false : (boolean) variant.getValue()); + return response.isPresent() ? response.get() : false; } @Override - public Map getAttributes() { - Variant response = getProperty("Attributes"); - if (response == null) return null; - return (Map) response.getValue(); + public Optional> getAttributes() { + return getProperty("Attributes").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((Map) variant.getValue())); } @Override - public void setAttributes(Map attributes) { - setProperty("Attributes", new Variant(attributes, "a{ss}")); + public boolean setAttributes(Map attributes) { + return setProperty("Attributes", new Variant(attributes, "a{ss}")); } @Override - public String getLabel() { - Variant response = getProperty("Label"); - if (response == null) return null; - return (String) response.getValue(); + public Optional getLabel() { + return getProperty("Label").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((String) variant.getValue())); } @Override - public void setLabel(String label) { - setProperty("Label", new Variant(label)); + public boolean setLabel(String label) { + return setProperty("Label", new Variant(label)); } @Override - public String getType() { - Variant response = getProperty("Type"); - if (response == null) return null; - return (String) response.getValue(); + public Optional getType() { + return getProperty("Type").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((String) variant.getValue())); } @Override - public UInt64 created() { - Variant response = getProperty("Created"); - if (response == null) return null; - return (UInt64) response.getValue(); + public Optional created() { + return getProperty("Created").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((UInt64) variant.getValue())); } @Override - public UInt64 modified() { - Variant response = getProperty("Modified"); - if (response == null) return null; - return (UInt64) response.getValue(); + public Optional modified() { + return getProperty("Modified").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((UInt64) variant.getValue())); } @Override diff --git a/src/main/java/de/swiesend/secretservice/Prompt.java b/src/main/java/de/swiesend/secretservice/Prompt.java index 5676df2..da79f38 100644 --- a/src/main/java/de/swiesend/secretservice/Prompt.java +++ b/src/main/java/de/swiesend/secretservice/Prompt.java @@ -8,6 +8,7 @@ import java.time.Duration; import java.util.Arrays; import java.util.List; +import java.util.Optional; import static de.swiesend.secretservice.Static.DEFAULT_PROMPT_TIMEOUT; import static de.swiesend.secretservice.Static.ObjectPaths.PROMPT; @@ -24,13 +25,13 @@ public Prompt(Service service) { } @Override - public void prompt(String window_id) { + public boolean prompt(String window_id) { objectPath = Static.ObjectPaths.prompt(window_id); - send("Prompt", "s", window_id); + return send("Prompt", "s", window_id).isPresent(); } @Override - public void prompt(ObjectPath prompt) throws NoSuchObject { + public boolean prompt(ObjectPath prompt) { objectPath = prompt.getPath(); String windowID = ""; @@ -41,10 +42,11 @@ public void prompt(ObjectPath prompt) throws NoSuchObject { windowID = split[split.length - 1]; } } catch (IndexOutOfBoundsException | NullPointerException e) { - throw new NoSuchObject(objectPath); + // TODO: check if it is possible to can call the prompt anyway + return false; } - send("Prompt", "s", windowID); + return send("Prompt", "s", windowID).isPresent(); } /** @@ -82,10 +84,9 @@ public Completed await(ObjectPath path) { return await(path, DEFAULT_PROMPT_TIMEOUT); } - @Override - public void dismiss() { - send("Dismiss", ""); + public boolean dismiss() { + return send("Dismiss", "").isPresent(); } @Override diff --git a/src/main/java/de/swiesend/secretservice/Service.java b/src/main/java/de/swiesend/secretservice/Service.java index b02e906..903dc5b 100644 --- a/src/main/java/de/swiesend/secretservice/Service.java +++ b/src/main/java/de/swiesend/secretservice/Service.java @@ -6,15 +6,13 @@ import org.freedesktop.dbus.types.Variant; import de.swiesend.secretservice.handlers.Messaging; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; public class Service extends Messaging implements de.swiesend.secretservice.interfaces.Service { public static final List> signals = Arrays.asList( CollectionCreated.class, CollectionChanged.class, CollectionDeleted.class); + private static final Logger log = LoggerFactory.getLogger(Service.class); private Session session = null; public Service(DBusConnection connection) { @@ -25,88 +23,88 @@ public Service(DBusConnection connection) { } @Override - public Pair, ObjectPath> openSession(String algorithm, Variant input) { - Object[] params = send("OpenSession", "sv", algorithm, input); - if (params == null) return null; - session = new Session((ObjectPath) params[1], this); - return new Pair(params[0], params[1]); + public Optional, ObjectPath>> openSession(String algorithm, Variant input) { + return send("OpenSession", "sv", algorithm, input) + .filter(response -> !Static.Utils.isNullOrEmpty(response) && response.length == 2) + .flatMap(response -> Optional.of(new Pair, ObjectPath>((Variant) response[0], (ObjectPath) response[1]))) + .map(pair -> { + log.debug("Got session: " + pair.b.getPath()); + session = new Session(pair.b, this); + return pair; + }); } @Override - public Pair createCollection(Map properties, String alias) { - String a; - if (alias == null) { - a = ""; - } else { - a = alias; - } - Object[] params = send("CreateCollection", "a{sv}s", properties, a); - if (params == null) return null; - return new Pair(params[0], params[1]); + public Optional> createCollection(Map properties, String alias) { + String a = alias == null ? "" : alias; + return send("CreateCollection", "a{sv}s", properties, a).flatMap(response -> + (Static.Utils.isNullOrEmpty(response) || response.length != 2) ? + Optional.empty() : + Optional.of(new Pair((ObjectPath) response[0], (ObjectPath) response[1]))); } @Override - public Pair createCollection(Map properties) { + public Optional> createCollection(Map properties) { return createCollection(properties, ""); } @Override - public Pair, List> searchItems(Map attributes) { - Object[] params = send("SearchItems", "a{ss}", attributes); - if (params == null) return null; - return new Pair(params[0], params[1]); + public Optional, List>> searchItems(Map attributes) { + return send("SearchItems", "a{ss}", attributes).flatMap(response -> + (Static.Utils.isNullOrEmpty(response) || response.length != 2) ? + Optional.empty() : + Optional.of(new Pair, List>((List) response[0], (List) response[1]))); } @Override - public Pair, ObjectPath> unlock(List objects) { - Object[] params = send("Unlock", "ao", objects); - if (params == null) return null; - return new Pair(params[0], params[1]); + public Optional, ObjectPath>> unlock(List objects) { + return send("Unlock", "ao", objects).flatMap(response -> + (Static.Utils.isNullOrEmpty(response) || response.length != 2) ? + Optional.empty() : + Optional.of(new Pair, ObjectPath>((List) response[0], (ObjectPath) response[1]))); } @Override - public Pair, ObjectPath> lock(List objects) { - Object[] params = send("Lock", "ao", objects); - if (params == null) return null; - return new Pair(params[0], params[1]); + public Optional, ObjectPath>> lock(List objects) { + return send("Lock", "ao", objects).flatMap(response -> + (Static.Utils.isNullOrEmpty(response) || response.length != 2) ? + Optional.empty() : + Optional.of(new Pair, ObjectPath>((List) response[0], (ObjectPath) response[1]))); } @Override - public void lockService() { - send("LockService", ""); + public boolean lockService() { + return send("LockService", "").isPresent(); } @Override - public ObjectPath changeLock(ObjectPath collection) { - Object[] params = send("ChangeLock", "o", collection); - if (params == null) return null; - return (ObjectPath) params[0]; + public Optional changeLock(ObjectPath collection) { + return send("ChangeLock", "o", collection).flatMap(response -> + Static.Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.of((ObjectPath) response[0])); } @Override - public Map getSecrets(List items, ObjectPath session) { - Object[] params = send("GetSecrets", "aoo", items, session); - if (params == null) return null; - return (Map) params[0]; + public Optional> getSecrets(List items, ObjectPath session) { + return send("GetSecrets", "aoo", items, session).flatMap(response -> + Static.Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.of((Map) response[0])); } @Override - public ObjectPath readAlias(String name) { - Object[] params = send("ReadAlias", "s", name); - if (params == null) return null; - return (ObjectPath) params[0]; + public Optional readAlias(String name) { + return send("ReadAlias", "s", name).flatMap(response -> + Static.Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.of((ObjectPath) response[0])); } @Override - public void setAlias(String name, ObjectPath collection) { - send("SetAlias", "so", name, collection); + public boolean setAlias(String name, ObjectPath collection) { + return send("SetAlias", "so", name, collection).isPresent(); } @Override - public List getCollections() { - Variant response = getProperty("Collections"); - if (response == null) return null; - return (ArrayList) response.getValue(); + public Optional> getCollections() { + return getProperty("Collections").flatMap(variant -> + variant == null ? Optional.empty() : Optional.ofNullable((ArrayList) variant.getValue()) + ); } @Override diff --git a/src/main/java/de/swiesend/secretservice/Session.java b/src/main/java/de/swiesend/secretservice/Session.java index ea23398..a42e461 100644 --- a/src/main/java/de/swiesend/secretservice/Session.java +++ b/src/main/java/de/swiesend/secretservice/Session.java @@ -18,8 +18,8 @@ public Session(String session_id, Service service) { } @Override - public void close() { - send("Close"); + public boolean close() { + return send("Close").isPresent(); } @Override diff --git a/src/main/java/de/swiesend/secretservice/Static.java b/src/main/java/de/swiesend/secretservice/Static.java index 1e26b4c..8fdd906 100644 --- a/src/main/java/de/swiesend/secretservice/Static.java +++ b/src/main/java/de/swiesend/secretservice/Static.java @@ -7,19 +7,12 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Optional; public class Static { public static final Duration DEFAULT_PROMPT_TIMEOUT = Duration.ofSeconds(120); - public static boolean isNullOrEmpty(final CharSequence cs) { - return cs == null || cs.toString().trim().isEmpty(); - } - - public static boolean isNullOrEmpty(final String s) { - return s == null || s.trim().isEmpty(); - } - public static class DBus { public static final Long DEFAULT_DELAY_MILLIS = 100L; @@ -181,6 +174,14 @@ public static ObjectPath toObjectPath(String path) { return new ObjectPath("", path); } + public static Optional toObjectPath(Object obj) { + try { + return Optional.of((ObjectPath) obj); + } catch (ClassCastException e) { + return Optional.empty(); + } + } + public static List toStrings(List paths) { ArrayList ps = new ArrayList(); for (ObjectPath p : paths) { @@ -199,6 +200,20 @@ public static List toDBusPaths(List paths) { } + public static class Utils { + public static boolean isNullOrEmpty(final CharSequence cs) { + return cs == null || cs.toString().trim().isEmpty(); + } + + public static boolean isNullOrEmpty(final String s) { + return s == null || s.trim().isEmpty(); + } + + public static boolean isNullOrEmpty(Object[] objects) { + return objects == null || objects.length == 0; + } + } + /** * RFC 2409: https://tools.ietf.org/html/rfc2409 */ diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index 8c358c9..388c46b 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.security.*; import java.security.spec.InvalidKeySpecException; +import java.util.Optional; public class TransportEncryption implements AutoCloseable { @@ -78,12 +79,12 @@ public boolean openSession() throws DBusException { BigInteger ya = ((DHPublicKey) publicKey).getY(); // open session with "Client DH pub key as an array of bytes" without prime or generator - Pair, ObjectPath> osResponse = service.openSession( + Optional, ObjectPath>> osResponse = service.openSession( Static.Algorithm.DH_IETF1024_SHA256_AES128_CBC_PKCS7, new Variant(ya.toByteArray())); // transform peer's raw Y to a public key - if (osResponse != null) { - yb = osResponse.a.getValue(); + if (osResponse.isPresent()) { + yb = osResponse.get().a.getValue(); return true; } else { return false; diff --git a/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java b/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java index 4a797ce..3234933 100644 --- a/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java +++ b/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java @@ -1,14 +1,15 @@ package de.swiesend.secretservice.gnome.keyring; -import de.swiesend.secretservice.Static; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.types.Variant; import de.swiesend.secretservice.Secret; import de.swiesend.secretservice.Service; +import de.swiesend.secretservice.Static; import de.swiesend.secretservice.handlers.Messaging; import java.util.Map; +import java.util.Optional; public class InternalUnsupportedGuiltRiddenInterface extends Messaging implements de.swiesend.secretservice.gnome.keyring.interfaces.InternalUnsupportedGuiltRiddenInterface { @@ -21,25 +22,33 @@ public InternalUnsupportedGuiltRiddenInterface(Service service) { } @Override - public void changeWithMasterPassword(DBusPath collection, Secret original, Secret master) { - send("ChangeWithMasterPassword", "o(oayays)(oayays)", collection, original, master); + public boolean changeWithMasterPassword(DBusPath collection, Secret original, Secret master) { + return send("ChangeWithMasterPassword", "o(oayays)(oayays)", collection, original, master) + // TODO: check which condition should apply + // .map(Static.Utils::isNullOrEmpty) + .map(response -> !Static.Utils.isNullOrEmpty(response)) + .orElse(false); } @Override - public ObjectPath changeWithPrompt(DBusPath collection) { - Object[] response = send("ChangeWithPrompt", "o", collection); - return (ObjectPath) response[0]; + public Optional changeWithPrompt(DBusPath collection) { + return send("ChangeWithPrompt", "o", collection) + .flatMap(response -> Static.Convert.toObjectPath(response[0])); } @Override - public ObjectPath createWithMasterPassword(Map properties, Secret master) { - Object[] response = send("CreateWithMasterPassword", "a{sv}(oayays)", properties, master); - return (ObjectPath) response[0]; + public Optional createWithMasterPassword(Map properties, Secret master) { + return send("CreateWithMasterPassword", "a{sv}(oayays)", properties, master) + .flatMap(response -> Static.Convert.toObjectPath(response[0])); } @Override - public void unlockWithMasterPassword(DBusPath collection, Secret master) { - send("UnlockWithMasterPassword", "o(oayays)", collection, master); + public boolean unlockWithMasterPassword(DBusPath collection, Secret master) { + return send("UnlockWithMasterPassword", "o(oayays)", collection, master) + // TODO: check which condition should apply + // .map(Static.Utils::isNullOrEmpty) + .map(response -> !Static.Utils.isNullOrEmpty(response)) + .orElse(false); } @Override diff --git a/src/main/java/de/swiesend/secretservice/gnome/keyring/interfaces/InternalUnsupportedGuiltRiddenInterface.java b/src/main/java/de/swiesend/secretservice/gnome/keyring/interfaces/InternalUnsupportedGuiltRiddenInterface.java index 6a7b5f1..f281508 100644 --- a/src/main/java/de/swiesend/secretservice/gnome/keyring/interfaces/InternalUnsupportedGuiltRiddenInterface.java +++ b/src/main/java/de/swiesend/secretservice/gnome/keyring/interfaces/InternalUnsupportedGuiltRiddenInterface.java @@ -8,6 +8,7 @@ import de.swiesend.secretservice.Secret; import java.util.Map; +import java.util.Optional; @DBusInterfaceName("org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface") public interface InternalUnsupportedGuiltRiddenInterface extends DBusInterface { @@ -21,7 +22,7 @@ public interface InternalUnsupportedGuiltRiddenInterface extends DBusInterface { * @param original The current password. * @param master The new password. */ - void changeWithMasterPassword(DBusPath collection, Secret original, Secret master); + boolean changeWithMasterPassword(DBusPath collection, Secret original, Secret master); /** * Toggle the lock of a collection. @@ -30,7 +31,7 @@ public interface InternalUnsupportedGuiltRiddenInterface extends DBusInterface { * * @return The ObjectPath of the collection. */ - ObjectPath changeWithPrompt(DBusPath collection); + Optional changeWithPrompt(DBusPath collection); /** * Create a collection with a password without prompting. @@ -40,7 +41,7 @@ public interface InternalUnsupportedGuiltRiddenInterface extends DBusInterface { * * @return The ObjectPath of the created collection. */ - ObjectPath createWithMasterPassword(Map properties, Secret master); + Optional createWithMasterPassword(Map properties, Secret master); /** * Unlock a collection without prompting. @@ -48,6 +49,6 @@ public interface InternalUnsupportedGuiltRiddenInterface extends DBusInterface { * @param collection The ObjectPath of the collection. * @param master The password of the collection. */ - void unlockWithMasterPassword(DBusPath collection, Secret master); + boolean unlockWithMasterPassword(DBusPath collection, Secret master); } diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 17e9d37..32d2166 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -1,6 +1,5 @@ package de.swiesend.secretservice.handlers; -import org.apache.commons.lang3.ArrayUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.messages.MethodCall; @@ -19,6 +18,7 @@ public class MessageHandler { private Logger log = LoggerFactory.getLogger(getClass()); private DBusConnection connection; + private boolean fireAndForget = true; public MessageHandler(DBusConnection connection) { this.connection = connection; @@ -48,7 +48,7 @@ public Optional send(String service, String path, String iface, String switch (error) { case "org.freedesktop.Secret.Error.NoSession": case "org.freedesktop.Secret.Error.NoSuchObject": - if (ArrayUtils.isEmpty(parameters)) { + if (Static.Utils.isNullOrEmpty(parameters)) { log.warn(error); } else { log.warn(error + ": " + parameters[0]); @@ -56,7 +56,7 @@ public Optional send(String service, String path, String iface, String return Optional.empty(); case "org.gnome.keyring.Error.Denied": case "org.freedesktop.Secret.Error.IsLocked": - if (ArrayUtils.isEmpty(parameters)) { + if (Static.Utils.isNullOrEmpty(parameters)) { log.info(error); } else { log.info(error + ": " + parameters[0]); @@ -75,7 +75,6 @@ public Optional send(String service, String path, String iface, String return Optional.empty(); } } - return Optional.ofNullable(parameters); } catch (DBusException e) { log.error("Unexpected D-Bus response: ", e); @@ -90,7 +89,7 @@ public Optional getProperty(String service, String path, String iface, "Get", "ss", iface, property); if (!maybeResponse.isPresent()) return Optional.empty(); Object[] response = maybeResponse.get(); - return ArrayUtils.isEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); + return Static.Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); } public Optional getAllProperties(String service, String path, String iface) { @@ -98,11 +97,28 @@ public Optional getAllProperties(String service, String path, String if "GetAll", "ss", iface); if (!maybeResponse.isPresent()) return Optional.empty(); Object[] response = maybeResponse.get(); - return ArrayUtils.isEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); + return Static.Utils.isNullOrEmpty(response) ? Optional.empty() : Optional.ofNullable((Variant) response[0]); } public boolean setProperty(String service, String path, String iface, String property, Variant value) { Optional maybeResponse = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, "Set", "ssv", iface, property, value); + // TODO: resolve return value +// if (maybeResponse.isPresent() && !fireAndForget) { +// Optional maybeValue = getProperty(service, path, iface, property); +// if (maybeValue.isPresent()) { +// Variant result = maybeValue.get(); +// if (result == null) return false; +// boolean valueCond = value.getValue() == result.getValue(); +// boolean signatureCond = value.getSig() == result.getSig(); +// boolean typeCond = value.getType() == result.getType(); +// return valueCond && signatureCond && typeCond; +// } else { +// return false; +// } +// } else { +// return maybeResponse.isPresent(); +// } + //return true; return maybeResponse.isPresent(); } diff --git a/src/main/java/de/swiesend/secretservice/interfaces/Collection.java b/src/main/java/de/swiesend/secretservice/interfaces/Collection.java index 417250e..ad0d71b 100644 --- a/src/main/java/de/swiesend/secretservice/interfaces/Collection.java +++ b/src/main/java/de/swiesend/secretservice/interfaces/Collection.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; @DBusInterfaceName(Static.Interfaces.COLLECTION) public interface Collection extends DBusInterface { @@ -21,113 +22,56 @@ public interface Collection extends DBusInterface { /** * The key of the D-Bus properties for the label of a collection. */ - public static final String LABEL = "org.freedesktop.Secret.Collection.Label"; - - public static class ItemCreated extends DBusSignal { - public final DBusPath item; - - /** - * A new item in this collection was created. - * - * @param path The path to the object this is emitted from. - * @param item The item that was created. - * - * @throws DBusException Could not communicate properly with the D-Bus. - */ - public ItemCreated(String path, DBusPath item) throws DBusException { - super(path, item); - this.item = item; - } - } - - public static class ItemDeleted extends DBusSignal { - public final DBusPath item; - - /** - * An item in this collection was deleted. - * - * @param path The path to the object this is emitted from. - * @param item The item that was deleted. - * - * @throws DBusException Could not communicate properly with the D-Bus. - */ - public ItemDeleted(String path, DBusPath item) throws DBusException { - super(path, item); - this.item = item; - } - } - - public static class ItemChanged extends DBusSignal { - public final DBusPath item; - - /** - * An item in this collection changed. - * - * @param path The path to the object this is emitted from. - * @param item The item that was changed. - * - * @throws DBusException Could not communicate properly with the D-Bus. - */ - public ItemChanged(String path, DBusPath item) throws DBusException { - super(path, item); - this.item = item; - } - } + String LABEL = "org.freedesktop.Secret.Collection.Label"; /** * Delete this collection. * * @return prompt — A prompt to delete the collection, or the special value '/' when no prompt is necessary. - * * @see ObjectPath */ - abstract public ObjectPath delete(); + Optional delete(); /** * Search for items in this collection matching the lookup attributes. * - * @param attributes Attributes to match. - * + * @param attributes Attributes to match. * @return results — Items that matched the attributes. - * * @see ObjectPath * @see Secret * @see Item */ - abstract public List searchItems(Map attributes); + Optional> searchItems(Map attributes); /** * Create an item with the given attributes, secret and label. If replace is set, then it replaces an item already * present with the same values for the attributes. * - * @param properties The properties for the new item. - * - *

This allows setting the new item's properties upon its creation. All READWRITE properties - * are useable. Specify the property names in full interface.Property form.

- * - *

- * Example 13.2. Example for properties of an item:
- * - * properties = {
- *   "org.freedesktop.Secret.Item.Label": "MyItem",
- *   "org.freedesktop.Secret.Item.Attributes": {
- *     "Attribute1": "Value1",
- *     "Attribute2": "Value2"
- *   }
- * }
- *

- * - *

- * Note: - * Please note that there is a distinction between the terms Property, which refers - * to D-Bus properties of an object, and Attribute, which refers to one of a - * secret item's string-valued attributes. - *

- * - * @param secret The secret to store in the item, encoded with the included session. - * - * @param replace Whether to replace an item with the same attributes or not. - * + * @param properties The properties for the new item. + * + *

This allows setting the new item's properties upon its creation. All READWRITE properties + * are usable. Specify the property names in full interface.Property form.

+ * + *

+ * Example 13.2. Example for properties of an item:
+ * + * properties = {
+ *   "org.freedesktop.Secret.Item.Label": "MyItem",
+ *   "org.freedesktop.Secret.Item.Attributes": {
+ *     "Attribute1": "Value1",
+ *     "Attribute2": "Value2"
+ *   }
+ * }
+ *

+ * + *

+ * Note: + * Please note that there is a distinction between the terms Property, which refers + * to D-Bus properties of an object, and Attribute, which refers to one of a + * secret item's string-valued attributes. + *

+ * @param secret The secret to store in the item, encoded with the included session. + * @param replace Whether to replace an item with the same attributes or not. * @return Pair<item, prompt>
*
* item — The item created, or the special value '/' if a prompt is necessary.
@@ -143,58 +87,107 @@ public ItemChanged(String path, DBusPath item) throws DBusException { * @see Secret * @see Item */ - abstract public Pair createItem(Map properties, Secret secret, boolean replace); + Optional> createItem(Map properties, Secret secret, boolean replace); /** * Items is a D-Bus Property. * *

It is managed by using the org.freedesktop.DBus.Properties interface.

* - * @return Items in this collection. + * @return Items in this collection. */ - abstract public List getItems(); + Optional> getItems(); /** * Label is a D-Bus Property. * *

It is managed by using the org.freedesktop.DBus.Properties interface.

* - * @return The displayable label of this collection. + * @return The displayable label of this collection. * - *

- * Note: - * The displayable label can differ from the actual name of a collection. - *

+ *

+ * Note: + * The displayable label can differ from the actual name of a collection. + *

*/ - abstract public String getLabel(); + Optional getLabel(); /** * Label is a D-Bus Property. * *

It is managed by using the org.freedesktop.DBus.Properties interface.

* - * @param label The displayable label of this collection. + * @param label The displayable label of this collection. * - *

- * Note: - * The displayable label can differ from the actual name of a collection. - *

+ *

+ * Note: + * The displayable label can differ from the actual name of a collection. + *

+ * @return */ - abstract public void setLabel(String label); + boolean setLabel(String label); /** - * @return Whether the collection is locked and must be authenticated by the client application. + * @return Whether the collection is locked and must be authenticated by the client application. */ - abstract public boolean isLocked(); + boolean isLocked(); /** - * @return The unix time when the collection was created. + * @return The unix time when the collection was created. */ - abstract public UInt64 created(); + Optional created(); /** - * @return The unix time when the collection was last modified. + * @return The unix time when the collection was last modified. */ - abstract public UInt64 modified(); + Optional modified(); + + class ItemCreated extends DBusSignal { + public final DBusPath item; + + /** + * A new item in this collection was created. + * + * @param path The path to the object this is emitted from. + * @param item The item that was created. + * @throws DBusException Could not communicate properly with the D-Bus. + */ + public ItemCreated(String path, DBusPath item) throws DBusException { + super(path, item); + this.item = item; + } + } + + class ItemDeleted extends DBusSignal { + public final DBusPath item; + + /** + * An item in this collection was deleted. + * + * @param path The path to the object this is emitted from. + * @param item The item that was deleted. + * @throws DBusException Could not communicate properly with the D-Bus. + */ + public ItemDeleted(String path, DBusPath item) throws DBusException { + super(path, item); + this.item = item; + } + } + + class ItemChanged extends DBusSignal { + public final DBusPath item; + + /** + * An item in this collection changed. + * + * @param path The path to the object this is emitted from. + * @param item The item that was changed. + * @throws DBusException Could not communicate properly with the D-Bus. + */ + public ItemChanged(String path, DBusPath item) throws DBusException { + super(path, item); + this.item = item; + } + } } diff --git a/src/main/java/de/swiesend/secretservice/interfaces/Item.java b/src/main/java/de/swiesend/secretservice/interfaces/Item.java index 8c2ec99..3e26382 100644 --- a/src/main/java/de/swiesend/secretservice/interfaces/Item.java +++ b/src/main/java/de/swiesend/secretservice/interfaces/Item.java @@ -8,6 +8,7 @@ import de.swiesend.secretservice.Static; import java.util.Map; +import java.util.Optional; @DBusInterfaceName(Static.Interfaces.ITEM) public interface Item extends DBusInterface { @@ -15,19 +16,19 @@ public interface Item extends DBusInterface { /** * The key of of the D-Bus properties for the label of an item. */ - public static final String LABEL = "org.freedesktop.Secret.Item.Label"; + String LABEL = "org.freedesktop.Secret.Item.Label"; /** * The key of the D-Bus properties for the attributes of an item. */ - public static final String ATTRIBUTES = "org.freedesktop.Secret.Item.Attributes"; + String ATTRIBUTES = "org.freedesktop.Secret.Item.Attributes"; /** * Delete this item. * * @return Prompt — A prompt objectpath, or the special value '/' if no prompt is necessary. */ - abstract public ObjectPath delete(); + Optional delete(); /** * Retrieve the secret for this item. @@ -35,19 +36,20 @@ public interface Item extends DBusInterface { * @param session The session to use to encode the secret. * @return secret — The secret retrieved. */ - abstract public Secret getSecret(ObjectPath session); + Optional getSecret(ObjectPath session); /** * Set the secret for this item. * * @param secret The secret to set, encoded for the included session. + * @return */ - abstract public void setSecret(Secret secret); + boolean setSecret(Secret secret); /** * @return Whether the item is locked and requires authentication, or not. */ - abstract public boolean isLocked(); + boolean isLocked(); /** * The lookup attributes for this item. @@ -58,7 +60,7 @@ public interface Item extends DBusInterface { * * @return The attributes of the item. */ - abstract public Map getAttributes(); + Optional> getAttributes(); /** @@ -69,8 +71,9 @@ public interface Item extends DBusInterface { *

It is managed by using the org.freedesktop.DBus.Properties interface.

* * @param attributes The attributes of the item. + * @return */ - abstract public void setAttributes(Map attributes); + boolean setAttributes(Map attributes); /** * Label is a D-Bus Property. @@ -84,7 +87,7 @@ public interface Item extends DBusInterface { * The displayable label can differ from the actual name of a collection. *

*/ - abstract public String getLabel(); + Optional getLabel(); /** @@ -98,22 +101,23 @@ public interface Item extends DBusInterface { * Note: * The displayable label can differ from the actual name of a collection. *

+ * @return */ - abstract public void setLabel(String label); + boolean setLabel(String label); /** * @return The "xdg:schema" of the item attributes. */ - abstract public String getType(); + Optional getType(); /** * @return The unix time when the item was created. */ - abstract public UInt64 created(); + Optional created(); /** * @return The unix time when the item was last modified. */ - abstract public UInt64 modified(); + Optional modified(); } diff --git a/src/main/java/de/swiesend/secretservice/interfaces/Prompt.java b/src/main/java/de/swiesend/secretservice/interfaces/Prompt.java index 8ff525a..c128dbf 100644 --- a/src/main/java/de/swiesend/secretservice/interfaces/Prompt.java +++ b/src/main/java/de/swiesend/secretservice/interfaces/Prompt.java @@ -12,7 +12,7 @@ @DBusInterfaceName(Static.Interfaces.PROMPT) public interface Prompt extends DBusInterface { - public static class Completed extends DBusSignal { + class Completed extends DBusSignal { public final boolean dismissed; public final Variant result; @@ -40,21 +40,20 @@ public Completed(String path, boolean dismissed, Variant result) throws DBusExce * * @see Completed */ - abstract public void prompt(String window_id); + boolean prompt(String window_id); /** * Perform the prompt. * * @param prompt Objectpath of the prompt. - * @throws NoSuchObject No such item or collection exists. * * @see Completed */ - abstract public void prompt(ObjectPath prompt) throws NoSuchObject; + boolean prompt(ObjectPath prompt); /** * Dismiss the prompt. */ - abstract public void dismiss(); + boolean dismiss(); } diff --git a/src/main/java/de/swiesend/secretservice/interfaces/Service.java b/src/main/java/de/swiesend/secretservice/interfaces/Service.java index 9a9557a..200de95 100644 --- a/src/main/java/de/swiesend/secretservice/interfaces/Service.java +++ b/src/main/java/de/swiesend/secretservice/interfaces/Service.java @@ -14,11 +14,12 @@ import java.util.List; import java.util.Map; +import java.util.Optional; @DBusInterfaceName(Static.Interfaces.SERVICE) public interface Service extends DBusInterface { - public static class CollectionCreated extends DBusSignal { + class CollectionCreated extends DBusSignal { public final DBusPath collection; /** @@ -36,7 +37,7 @@ public CollectionCreated(String path, DBusPath collection) throws DBusException } } - public static class CollectionDeleted extends DBusSignal { + class CollectionDeleted extends DBusSignal { public final DBusPath collection; /** @@ -54,7 +55,7 @@ public CollectionDeleted(String path, DBusPath collection) throws DBusException } } - public static class CollectionChanged extends DBusSignal { + class CollectionChanged extends DBusSignal { public final DBusPath collection; /** @@ -77,7 +78,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * * @param algorithm The algorithm the caller wishes to use. * @param input Input arguments for the algorithm. - * + * * @return Pair<output, result>
*
* output — Output of the session algorithm negotiation.
@@ -88,7 +89,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see Variant * @see ObjectPath */ - abstract public Pair, ObjectPath> openSession(String algorithm, Variant input); + Optional, ObjectPath>> openSession(String algorithm, Variant input); /** * Create a new collection with the specified properties. @@ -101,15 +102,15 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException *

* properties = { "org.freedesktop.Secret.Collection.Label": "MyCollection" } *

- * - * @param alias If creating this connection for a well known alias then a string like "default". - * If an collection with this well-known alias already exists, then that collection will be - * returned instead of creating a new collection. Any readwrite properties provided to this + * + * @param alias If creating this connection for a well known alias then a string like "default". + * If an collection with this well-known alias already exists, then that collection will be + * returned instead of creating a new collection. Any readwrite properties provided to this * function will be set on the collection.
*
- * Set this to an empty string if the new collection should not be associated with a well + * Set this to an empty string if the new collection should not be associated with a well * known alias. - * + * * @return Pair<collection, prompt>
*
* collection — The new collection object, or '/' if prompting is necessary.
@@ -120,7 +121,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see Variant * @see ObjectPath */ - abstract public Pair createCollection(Map properties, String alias); + Optional> createCollection(Map properties, String alias); /** * Create a new collection with the specified properties. @@ -133,7 +134,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException *

* properties = { "org.freedesktop.Secret.Collection.Label": "MyCollection" } *

- * + * * @return Pair<collection, prompt>
*
* collection — The new collection object, or '/' if prompting is necessary.
@@ -144,13 +145,13 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see Variant * @see ObjectPath */ - abstract public Pair createCollection(Map properties); + Optional> createCollection(Map properties); /** * Find items in any collection. * * @param attributes Find secrets in any collection. - * + * *

* Example: * { @@ -158,14 +159,14 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * "Attribute2": "Value2" * } *

- * + * *

* Note: - * Please note that there is a distinction between the terms Property, which refers - * to D-Bus properties of an object, and Attribute, which refers to one of a + * Please note that there is a distinction between the terms Property, which refers + * to D-Bus properties of an object, and Attribute, which refers to one of a * secret item's string-valued attributes. *

- * + * * @return Pair<unlocked, locked>
*
* unlocked — Items found.
@@ -175,13 +176,13 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see Pair * @see ObjectPath */ - abstract public Pair, List> searchItems(Map attributes); + Optional, List>> searchItems(Map attributes); /** * Unlock the specified objects. * * @param objects Objects to unlock. - * + * * @return Pair<unlocked, prompt>
*
* unlocked — Objects that were unlocked without a prompt.
@@ -191,13 +192,13 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see Pair * @see ObjectPath */ - abstract public Pair, ObjectPath> unlock(List objects); + Optional, ObjectPath>> unlock(List objects); /** * Lock the items. * * @param objects Objects to lock. - * + * * @return Pair<locked, prompt>
*
* locked — Objects that were locked without a prompt.
@@ -206,7 +207,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * * @see Pair */ - abstract public Pair, ObjectPath> lock(List objects); + Optional, ObjectPath>> lock(List objects); /** * Lock the entire Secret Service API. @@ -217,7 +218,7 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * {@link #unlock(List objects)}
* {@link InternalUnsupportedGuiltRiddenInterface#unlockWithMasterPassword(DBusPath collection, Secret master)}
*/ - abstract public void lockService(); + boolean lockService(); /** * Toggle the lock for a collection with a prompt. @@ -230,34 +231,34 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * See Also:
* {@link InternalUnsupportedGuiltRiddenInterface#changeWithPrompt(DBusPath collection)}
*/ - abstract public ObjectPath changeLock(ObjectPath collection); + Optional changeLock(ObjectPath collection); /** * Retrieve multiple secrets from different items. * * @param items Items to get secrets for. - * + * * @param session The session to use to encode the secrets. - * + * * @return secrets — Secrets for the items. * * @see Secret * @see ObjectPath */ - abstract public Map getSecrets(List items, ObjectPath session); + Optional> getSecrets(List items, ObjectPath session); /** * Get the collection with the given alias. * * @param name An alias, such as 'default'. - * + * * @return collection — The collection or the the path '/' if no such collection exists. * * @see Static.ObjectPaths * @see ObjectPath * @see Collection */ - abstract public ObjectPath readAlias(String name); + Optional readAlias(String name); /** * Setup a collection alias. @@ -269,11 +270,11 @@ public CollectionChanged(String path, DBusPath collection) throws DBusException * @see ObjectPath * @see Collection */ - abstract public void setAlias(String name, ObjectPath collection); + boolean setAlias(String name, ObjectPath collection); /** * @return A list of present collections. */ - abstract public List getCollections(); + Optional> getCollections(); } diff --git a/src/main/java/de/swiesend/secretservice/interfaces/Session.java b/src/main/java/de/swiesend/secretservice/interfaces/Session.java index 64ceacc..b131926 100644 --- a/src/main/java/de/swiesend/secretservice/interfaces/Session.java +++ b/src/main/java/de/swiesend/secretservice/interfaces/Session.java @@ -9,7 +9,9 @@ public interface Session extends DBusInterface { /** * Close this session. + * + * @return true if closed else false */ - abstract public void close(); + boolean close(); } diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 140a9fa..0c76b50 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -32,12 +32,13 @@ import static de.swiesend.secretservice.Static.DBus.DEFAULT_DELAY_MILLIS; import static de.swiesend.secretservice.Static.DBus.MAX_DELAY_MILLIS; import static de.swiesend.secretservice.Static.DEFAULT_PROMPT_TIMEOUT; +import static de.swiesend.secretservice.Static.Utils; public final class SimpleCollection extends de.swiesend.secretservice.simple.interfaces.SimpleCollection { private static final Logger log = LoggerFactory.getLogger(SimpleCollection.class); - private static Thread shutdownHook = setupShutdownHook(); - private static DBusConnection connection = getConnection(); + private static final DBusConnection connection = getConnection(); + private static final Thread shutdownHook = setupShutdownHook(); private TransportEncryption transport = null; private Service service = null; private Session session = null; @@ -102,13 +103,13 @@ public SimpleCollection(String label, CharSequence password) throws IOException Map properties = Collection.createProperties(label); if (password == null) { - Pair response = service.createCollection(properties); + Pair response = service.createCollection(properties).get(); if (!"/".equals(response.a.getPath())) { path = response.a; } performPrompt(response.b); - } else if (encrypted != null && withoutPrompt != null) { - path = withoutPrompt.createWithMasterPassword(properties, encrypted); + } else if (encrypted != null) { + path = withoutPrompt.createWithMasterPassword(properties, encrypted).get(); } if (path == null) { @@ -179,7 +180,8 @@ public static boolean isAvailable() { List names = Arrays.asList(bus.ListNames()); if (!(names.containsAll(Arrays.asList( Static.DBus.Service.DBUS, - Static.Service.SECRETS)))) { + Static.Service.SECRETS, + de.swiesend.secretservice.gnome.keyring.Static.Service.KEYRING)))) { return false; } @@ -297,12 +299,12 @@ private void init() throws IOException { } private Map getLabels() { - List collections = service.getCollections(); + List collections = service.getCollections().get(); Map labels = new HashMap(); for (ObjectPath path : collections) { Collection c = new Collection(path, service, null); - labels.put(path, c.getLabel()); + labels.put(path, c.getLabel().get()); } return labels; @@ -375,7 +377,7 @@ private void unlock() { withoutPrompt.unlockWithMasterPassword(collection.getPath(), encrypted); log.debug("Unlocked collection: " + collection.getLabel() + " (" + collection.getObjectPath() + ")"); } else { - Pair, ObjectPath> response = service.unlock(lockable()); + Pair, ObjectPath> response = service.unlock(lockable()).get(); performPrompt(response.b); if (!collection.isLocked()) { isUnlockedOnceWithUserPermission = true; @@ -441,7 +443,7 @@ public void close() { @Override public void delete() throws AccessControlException { if (!isDefault()) { - ObjectPath promptPath = collection.delete(); + ObjectPath promptPath = collection.delete().get(); performPrompt(promptPath); } else { throw new AccessControlException("Default collections may not be deleted with the simple API."); @@ -460,7 +462,7 @@ public void delete() throws AccessControlException { @Override public String createItem(String label, CharSequence password, Map attributes) throws IllegalArgumentException { - if (Static.isNullOrEmpty(password)) { + if (Utils.isNullOrEmpty(password)) { throw new IllegalArgumentException("The password may not be null or empty."); } if (label == null) { @@ -474,7 +476,7 @@ public String createItem(String label, CharSequence password, Map properties = Item.createProperties(label, attributes); try (final Secret secret = transport.encrypt(password)) { - Pair response = collection.createItem(properties, secret, false); + Pair response = collection.createItem(properties, secret, false).get(); if (response == null) return null; item = response.a; if ("/".equals(item.getPath())) { @@ -525,7 +527,7 @@ public String createItem(String label, CharSequence password) throws IllegalArgu @Override public void updateItem(String objectPath, String label, CharSequence password, Map attributes) throws IllegalArgumentException { - if (Static.isNullOrEmpty(objectPath)) { + if (Utils.isNullOrEmpty(objectPath)) { throw new IllegalArgumentException("The object path of the item may not be null or empty."); } @@ -561,9 +563,9 @@ public void updateItem(String objectPath, String label, CharSequence password, M */ @Override public String getLabel(String objectPath) { - if (Static.isNullOrEmpty(objectPath)) return null; + if (Utils.isNullOrEmpty(objectPath)) return null; unlock(); - return getItem(objectPath).getLabel(); + return getItem(objectPath).getLabel().get(); } /** @@ -576,9 +578,9 @@ public String getLabel(String objectPath) { */ @Override public Map getAttributes(String objectPath) { - if (Static.isNullOrEmpty(objectPath)) return null; + if (Utils.isNullOrEmpty(objectPath)) return null; unlock(); - return getItem(objectPath).getAttributes(); + return getItem(objectPath).getAttributes().get(); } /** @@ -592,7 +594,7 @@ public List getItems(Map attributes) { if (attributes == null) return null; unlock(); - List objects = collection.searchItems(attributes); + List objects = collection.searchItems(attributes).get(); if (objects != null && !objects.isEmpty()) { return Static.Convert.toStrings(objects); @@ -609,13 +611,15 @@ public List getItems(Map attributes) { */ @Override public char[] getSecret(String objectPath) { - if (Static.isNullOrEmpty(objectPath)) return null; + if (Utils.isNullOrEmpty(objectPath)) return null; unlock(); final Item item = getItem(objectPath); char[] decrypted = null; - try (final Secret secret = item.getSecret(session.getPath())) { + ObjectPath sessionPath = session.getPath(); + log.info(sessionPath.getPath()); + try (final Secret secret = item.getSecret(sessionPath).orElseGet(() -> new Secret(sessionPath, null))) { decrypted = transport.decrypt(secret); } catch (NoSuchPaddingException | NoSuchAlgorithmException | @@ -641,7 +645,7 @@ public char[] getSecret(String objectPath) { public Map getSecrets() throws AccessControlException { unlockWithUserPermission(); - List items = collection.getItems(); + List items = collection.getItems().get(); if (items == null) return null; Map passwords = new HashMap(); @@ -664,12 +668,12 @@ public Map getSecrets() throws AccessControlException { */ @Override public void deleteItem(String objectPath) throws AccessControlException { - if (Static.isNullOrEmpty(objectPath)) throw new AccessControlException("Cannot delete an unspecified item."); + if (Utils.isNullOrEmpty(objectPath)) throw new AccessControlException("Cannot delete an unspecified item."); unlockWithUserPermission(); Item item = getItem(objectPath); - ObjectPath promptPath = item.delete(); + ObjectPath promptPath = item.delete().get(); performPrompt(promptPath); } diff --git a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java index c73ebe5..7557fec 100644 --- a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java @@ -41,13 +41,13 @@ public void afterEach() { @Test @DisplayName("delete test collection") public void delete() { - List expected = context.service.getCollections(); - ObjectPath promptPath = context.collection.delete(); + List expected = context.service.getCollections().get(); + ObjectPath promptPath = context.collection.delete().get(); log.info(promptPath.toString()); assertEquals("/", promptPath.getPath()); // assertTrue(promptPath.getPath().startsWith("/org/freedesktop/secrets/prompt/p")); - List actual = context.service.getCollections(); + List actual = context.service.getCollections().get(); assertEquals(expected.size() - 1, actual.size()); } @@ -57,7 +57,7 @@ public void searchItems() { Map attributes = new HashMap(); attributes.put("Attribute1", "Value1"); - List items = context.collection.searchItems(attributes); + List items = context.collection.searchItems(attributes).get(); log.info(Arrays.toString(items.toArray())); assertEquals(1, items.size()); assertTrue(items.get(0).getPath().startsWith("/org/freedesktop/secrets/collection/test/")); @@ -75,22 +75,22 @@ public void createItem() { attributes.put("Attribute1", "Value1"); Map properties = Item.createProperties("TestItem", attributes); - Pair response = context.collection.createItem(properties, secret, true); + Pair response = context.collection.createItem(properties, secret, true).get(); log.info(response.toString()); assertTrue(response.a.getPath().startsWith("/org/freedesktop/secrets/collection/test/")); assertEquals("/", response.b.getPath()); - List items = context.collection.getItems(); + List items = context.collection.getItems().get(); assertEquals(1, items.size()); context.collection.createItem(properties, secret, false); - items = context.collection.getItems(); + items = context.collection.getItems().get(); assertEquals(2, items.size()); } @Test public void getItems() { - List items = context.collection.getItems(); + List items = context.collection.getItems().get(); log.info(Arrays.toString(items.toArray())); assertEquals(1, items.size()); assertTrue(items.get(0).getPath().startsWith("/org/freedesktop/secrets/collection/test/")); @@ -100,7 +100,7 @@ public void getItems() { @Disabled public void getLabel() { Collection collection = new Collection("login", context.service); - String response = collection.getLabel(); + String response = collection.getLabel().get(); log.info(response); List labels = Arrays.asList(new String[]{ "login", // en @@ -114,15 +114,15 @@ public void getLabel() { @Test public void setLabel() { - String label = context.collection.getLabel(); + String label = context.collection.getLabel().get(); assertEquals("test", label); context.collection.setLabel("test-renamed"); - label = context.collection.getLabel(); + label = context.collection.getLabel().get(); assertEquals("test-renamed", label); context.collection.setLabel("test"); - label = context.collection.getLabel(); + label = context.collection.getLabel().get(); assertEquals("test", label); } @@ -140,17 +140,17 @@ public void created() { UInt64 response; collection = new Collection("test", context.service); - response = collection.created(); + response = collection.created().get(); log.info("test: " + response); assertTrue(response.longValue() >= 0L); collection = new Collection("login", context.service); - response = collection.created(); + response = collection.created().get(); log.info("login: " + response); assertTrue(response.longValue() >= 0L); collection = new Collection("session", context.service); - response = collection.created(); + response = collection.created().get(); log.info("session: " + response); assertTrue(response.longValue() == 0L); } @@ -162,16 +162,16 @@ public void modified() { UInt64 response; collection = new Collection("test", context.service); - response = collection.modified(); + response = collection.modified().get(); log.info("test: " + response); assertTrue(response.longValue() >= 0L); collection = new Collection("login", context.service); - response = collection.modified(); + response = collection.modified().get(); log.info("login: " + response); assertTrue(response.longValue() >= 0L); collection = new Collection("session", context.service); - response = collection.modified(); + response = collection.modified().get(); log.info("session: " + response); assertTrue(response.longValue() == 0L); } diff --git a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java index ce0c5e9..3890f46 100644 --- a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java @@ -60,14 +60,14 @@ public void testWithTransportEncryption() throws InternalUnsupportedGuiltRiddenInterface noPrompt = new InternalUnsupportedGuiltRiddenInterface(service); Secret master = transportEncryption.encrypt("test"); Collection collection = new Collection("test", service); - List collections = Static.Convert.toStrings(service.getCollections()); + List collections = Static.Convert.toStrings(service.getCollections().get()); if (collections.contains(collection.getObjectPath())) { noPrompt.unlockWithMasterPassword(collection.getPath(), master); } else { HashMap properties = new HashMap(); properties.put("org.freedesktop.Secret.Collection.Label", new Variant("test")); - ObjectPath collectionPath = noPrompt.createWithMasterPassword(properties, master); + ObjectPath collectionPath = noPrompt.createWithMasterPassword(properties, master).get(); log.info("created collection: " + collectionPath.getPath()); } @@ -86,13 +86,13 @@ public void testWithTransportEncryption() throws noPrompt.unlockWithMasterPassword(collection.getPath(), master); } - Pair createItemResponse = collection.createItem(properties, encrypted, true); + Pair createItemResponse = collection.createItem(properties, encrypted, true).get(); log.info("await signal: Collection.ItemCreated"); Thread.currentThread().sleep(50L); ObjectPath itemPath = createItemResponse.a; Item item = new Item(itemPath, service); - Secret actual = item.getSecret(session.getPath()); + Secret actual = item.getSecret(session.getPath()).get(); assertEquals(encrypted.getSession(), actual.getSession()); assertEquals(encrypted.getContentType(), actual.getContentType()); diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index 771abc1..9a6924e 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -37,20 +37,20 @@ public void afterEach() { @Test @DisplayName("delete item") public void delete() { - List items = context.collection.getItems(); + List items = context.collection.getItems().get(); assertEquals(1, items.size()); - ObjectPath prompt = context.item.delete(); + ObjectPath prompt = context.item.delete().get(); // expect: no prompt assertEquals("/", prompt.getPath()); - items = context.collection.getItems(); + items = context.collection.getItems().get(); assertEquals(0, items.size()); } @Test public void getSecret() { - Secret secret = context.item.getSecret(context.session.getPath()); + Secret secret = context.item.getSecret(context.session.getPath()).get(); log.info(label("secret", secret.toString())); assertTrue(secret.getSession().getPath().startsWith("/org/freedesktop/secrets/session/s")); assertTrue(secret.getContentType().startsWith("text/plain")); @@ -76,9 +76,9 @@ public void getForeignSecret() { DBusPath alias = new DBusPath(Static.ObjectPaths.DEFAULT_COLLECTION); Collection login = new Collection(alias, context.service); - List items = login.getItems(); + List items = login.getItems().get(); Item item = new Item(items.get(0), context.service); - Secret secret = item.getSecret(context.service.getSession().getPath()); + Secret secret = item.getSecret(context.service.getSession().getPath()).get(); log.info(new String(secret.getSecretValue())); } @@ -87,7 +87,7 @@ public void setSecret() { Secret secret = new Secret(context.session.getPath(), "new secret".getBytes()); context.item.setSecret(secret); - Secret result = context.item.getSecret(context.session.getPath()); + Secret result = context.item.getSecret(context.session.getPath()).get(); log.info(label("secret", result.toString())); assertEquals("new secret", Static.Convert.toString(result.getSecretValue())); } @@ -101,7 +101,7 @@ public void isLocked() { @Test public void getAttributes() { - Map attributes = context.item.getAttributes(); + Map attributes = context.item.getAttributes().get(); log.info(attributes.toString()); assertTrue(attributes.size() > 0); assertEquals("Value1", attributes.get("Attribute1")); @@ -116,7 +116,7 @@ public void getAttributes() { @Test public void setAttributes() { - Map attributes = context.item.getAttributes(); + Map attributes = context.item.getAttributes().get(); log.info(context.item.getId()); log.info(attributes.toString()); assertTrue(attributes.size() == 3 || attributes.size() == 4); @@ -133,7 +133,7 @@ public void setAttributes() { context.item.setAttributes(attributes); - attributes = context.item.getAttributes(); + attributes = context.item.getAttributes().get(); log.info(context.item.getId()); log.info(attributes.toString()); @@ -148,7 +148,7 @@ public void setAttributes() { attributes = new HashMap(); attributes.put("Attribute1", "Value1"); - Pair, List> result = context.service.searchItems(attributes); + Pair, List> result = context.service.searchItems(attributes).get(); log.info(result.toString()); assertEquals(1, result.a.size()); } @@ -158,7 +158,7 @@ public void setAttributes() { */ @Test public void getLabel() { - String label = context.item.getLabel(); + String label = context.item.getLabel().get(); log.info(label("label", label)); assertEquals("TestItem", label); } @@ -166,14 +166,14 @@ public void getLabel() { @Test public void setLabel() { context.item.setLabel("RelabeledItem"); - String label = context.item.getLabel(); + String label = context.item.getLabel().get(); log.info(label("label", label)); assertEquals("RelabeledItem", label); } @Test public void getType() { - String type = context.item.getType(); + String type = context.item.getType().get(); log.info(type); if (!type.isEmpty()) { assertEquals("org.freedesktop.Secret.Generic", type); @@ -183,7 +183,7 @@ public void getType() { @Test @DisplayName("created at unixtime") public void created() { - UInt64 created = context.item.created(); + UInt64 created = context.item.created().get(); log.info(String.valueOf(created)); assertTrue(created.longValue() > 0L); } @@ -191,7 +191,7 @@ public void created() { @Test @DisplayName("modified at unixtime") public void modified() { - UInt64 modified = context.item.created(); + UInt64 modified = context.item.created().get(); log.info(String.valueOf(modified)); assertTrue(modified.longValue() >= 0L); } diff --git a/src/test/java/de/swiesend/secretservice/integration/PromptTest.java b/src/test/java/de/swiesend/secretservice/integration/PromptTest.java index 09ba406..4596550 100644 --- a/src/test/java/de/swiesend/secretservice/integration/PromptTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/PromptTest.java @@ -46,11 +46,11 @@ public void prompt() { cs.add(defaultCollection); log.info("lock default collection"); - Pair, ObjectPath> locked = context.service.lock(cs); + Pair, ObjectPath> locked = context.service.lock(cs).get(); log.info(locked.toString()); log.info("unlock default collection"); - Pair, ObjectPath> unlocked = context.service.unlock(cs); + Pair, ObjectPath> unlocked = context.service.unlock(cs).get(); log.info(unlocked.toString()); ObjectPath prompt = unlocked.b; @@ -68,7 +68,7 @@ public void dismissPrompt() throws InterruptedException { boolean expected = defaultCollection.isLocked(); Thread.currentThread().sleep(500L); - Pair, ObjectPath> response = context.service.unlock(cs); + Pair, ObjectPath> response = context.service.unlock(cs).get(); ObjectPath prompt = response.b; assertDoesNotThrow(() ->context.prompt.prompt(prompt)); // Should not throw NoSuchObject DBusSignal signal = handler.getLastHandledSignal(); diff --git a/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java b/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java index 6cd2daf..1ed0a36 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java @@ -37,13 +37,13 @@ public void afterEach() { public void openSession() { context.ensureService(); - Pair, ObjectPath> response = context.service.openSession(Static.Algorithm.PLAIN, new Variant("")); + Optional, ObjectPath>> response = context.service.openSession(Static.Algorithm.PLAIN, new Variant("")); log.info(response.toString()); - assertEquals("s", response.a.getSig()); - assertEquals("", response.a.getValue(), "the value of an empty byte[] behaves odd as it returns a String."); + assertEquals("s", response.get().a.getSig()); + assertEquals("", response.get().a.getValue(), "the value of an empty byte[] behaves odd as it returns a String."); - ObjectPath sessionPath = response.b; + ObjectPath sessionPath = response.get().b; assertTrue(sessionPath.getPath().startsWith("/org/freedesktop/secrets/session/s")); } @@ -71,14 +71,14 @@ public void openSessionWithTransportEncryption() { }; assertEquals(128, input.length); - Pair, ObjectPath> response = context.service.openSession( + Optional, ObjectPath>> response = context.service.openSession( Static.Algorithm.DH_IETF1024_SHA256_AES128_CBC_PKCS7, new Variant(input)); log.info(response.toString()); - byte[] peerPublicKey = response.a.getValue(); + byte[] peerPublicKey = response.get().a.getValue(); assertEquals(128, peerPublicKey.length); - ObjectPath sessionPath = response.b; + ObjectPath sessionPath = response.get().b; assertTrue(sessionPath.getPath().startsWith(Static.ObjectPaths.SESSION + "/s")); } @@ -87,15 +87,15 @@ public void openSessionWithTransportEncryption() { public void createCollection() { context.ensureCollection(); - ObjectPath deletePrompt = context.collection.delete(); + ObjectPath deletePrompt = context.collection.delete().orElse(new ObjectPath("", "/")); if (!deletePrompt.getPath().equals("/")) { context.prompt.await(deletePrompt); } - List before = context.service.getCollections(); + List before = context.service.getCollections().orElse(new ArrayList()); Map properties = Collection.createProperties("test"); - Pair response = context.service.createCollection(properties); + Pair response = context.service.createCollection(properties).get(); log.info(response.toString()); ObjectPath collectionPath = response.a; @@ -107,7 +107,7 @@ public void createCollection() { assertEquals("/", createPrompt.getPath()); } - List after = context.service.getCollections(); + List after = context.service.getCollections().get(); DBusSignal[] handled = context.prompt.getSignalHandler().getHandled(); Prompt.Completed completed = (Prompt.Completed) handled[0]; if (completed.dismissed) { @@ -124,7 +124,7 @@ public void searchItems() { Map attributes = new HashMap(); attributes.put("Attribute1", "Value1"); - Pair, List> response = context.service.searchItems(attributes); + Pair, List> response = context.service.searchItems(attributes).get(); List unlocked = toStrings(response.a); List locked = toStrings(response.b); @@ -149,14 +149,14 @@ public void unlockCollections() { ArrayList lockables = new ArrayList(); lockables.add(context.collection.getPath()); - response = context.service.lock(lockables); + response = context.service.lock(lockables).get(); log.info(response.toString()); locked = response.a; assertEquals(1, locked.size()); prompt = response.b; assertEquals("/", prompt.getPath()); - response = context.service.unlock(lockables); + response = context.service.unlock(lockables).get(); log.info(response.toString()); unlocked = response.a; assertEquals(0, unlocked.size()); @@ -178,16 +178,16 @@ public void unlockItems() { List locked, unlocked; ObjectPath prompt; - List items = context.collection.getItems(); + List items = context.collection.getItems().get(); - response = context.service.lock(items); + response = context.service.lock(items).get(); log.info(response.toString()); locked = response.a; assertEquals(1, locked.size()); prompt = response.b; assertEquals("/", prompt.getPath()); - response = context.service.unlock(items); + response = context.service.unlock(items).get(); log.info(response.toString()); unlocked = response.a; assertEquals(0, unlocked.size()); @@ -203,19 +203,18 @@ public void unlockItems() { @Test @Disabled public void lockCommonCollections() throws InterruptedException, NoSuchObject { + context.ensureSession(); // lock common collections: // * alias/default == collection/login // * collection/login // * collection/session - context.ensureSession(); - - ArrayList objects = new ArrayList(); - objects.add(Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION)); - objects.add(Static.Convert.toObjectPath(Static.ObjectPaths.LOGIN_COLLECTION)); - objects.add(Static.Convert.toObjectPath(Static.ObjectPaths.SESSION_COLLECTION)); + ArrayList collections = new ArrayList(); + collections.add(Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION)); + collections.add(Static.Convert.toObjectPath(Static.ObjectPaths.LOGIN_COLLECTION)); + collections.add(Static.Convert.toObjectPath(Static.ObjectPaths.SESSION_COLLECTION)); - Pair, ObjectPath> response = context.service.lock(objects); + Pair, ObjectPath> response = context.service.lock(collections).get(); log.info(response.toString()); List locked = response.a; @@ -226,9 +225,9 @@ public void lockCommonCollections() throws InterruptedException, NoSuchObject { ObjectPath prompt = response.b; assertEquals("/", prompt.getPath()); - for (int i = 0; i < objects.size(); i++) { - List unlock = Arrays.asList(new ObjectPath[]{objects.get(i)}); - response = context.service.unlock(unlock); + for (int i = 0; i < collections.size(); i++) { + List collection = Arrays.asList(new ObjectPath[]{collections.get(i)}); + response = context.service.unlock(collection).get(); prompt = response.b; if (!prompt.getPath().equals("/")) { context.prompt.await(prompt); @@ -251,19 +250,17 @@ public void changeLock() { ObjectPath result; obj = new ObjectPath("", Static.ObjectPaths.DEFAULT_COLLECTION); - result = context.service.changeLock(obj); + result = context.service.changeLock(obj).get(); log.info(result.toString()); assertTrue(result.getPath().startsWith("/org/freedesktop/secrets/prompt/")); - obj = new ObjectPath("", Static.ObjectPaths.LOGIN_COLLECTION); - result = context.service.changeLock(obj); + result = context.service.changeLock(obj).get(); log.info(result.toString()); assertTrue(result.getPath().startsWith("/org/freedesktop/secrets/prompt/")); - obj = new ObjectPath("", Static.ObjectPaths.SESSION_COLLECTION); - result = context.service.changeLock(obj); + result = context.service.changeLock(obj).get(); log.info(result.toString()); assertTrue(result.getPath().startsWith("/org/freedesktop/secrets/prompt/")); } @@ -272,8 +269,8 @@ public void changeLock() { public void getSecrets() { context.ensureItem(); - List items = context.collection.getItems(); - Map result = context.service.getSecrets(items, context.session.getPath()); + List items = context.collection.getItems().get(); + Map result = context.service.getSecrets(items, context.session.getPath()).get(); log.info(result.toString()); assertEquals(1, result.size()); @@ -285,20 +282,20 @@ public void readAlias() { ObjectPath collection; - collection = context.service.readAlias("default"); + collection = context.service.readAlias("default").get(); log.info(collection.toString()); assertEquals(Static.ObjectPaths.LOGIN_COLLECTION, collection.getPath(), "the default alias should point to the login collection"); - collection = context.service.readAlias("login"); + collection = context.service.readAlias("login").get(); log.info(collection.toString()); assertEquals(Static.ObjectPaths.LOGIN_COLLECTION, collection.getPath()); - collection = context.service.readAlias("session"); + collection = context.service.readAlias("session").get(); log.info(collection.toString()); assertEquals(Static.ObjectPaths.SESSION_COLLECTION, collection.getPath()); - collection = context.service.readAlias("test"); + collection = context.service.readAlias("test").get(); log.info(collection.toString()); assertEquals("/", collection.getPath(), "the test collection should not have an alias"); @@ -313,14 +310,14 @@ public void setAlias() { // change the default alias to point to the test collection context.service.setAlias("default", context.collection.getPath()); - collection = context.service.readAlias("default"); + collection = context.service.readAlias("default").get(); log.info("default: " + collection); assertEquals(context.collection.getPath().getPath(), collection.getPath()); // repair the default alias ObjectPath login = Static.Convert.toObjectPath(Static.ObjectPaths.LOGIN_COLLECTION); context.service.setAlias("default", login); - collection = context.service.readAlias("default"); + collection = context.service.readAlias("default").get(); log.info("default: " + collection); assertEquals(login.getPath(), collection.getPath()); } @@ -329,7 +326,7 @@ public void setAlias() { public void getCollections() { context.ensureCollection(); - List collections = context.service.getCollections(); + List collections = context.service.getCollections().get(); log.info(Arrays.toString(collections.toArray())); List cs = toStrings(collections); diff --git a/src/test/java/de/swiesend/secretservice/integration/StaticTest.java b/src/test/java/de/swiesend/secretservice/integration/StaticTest.java index 25c1c78..6ed0276 100644 --- a/src/test/java/de/swiesend/secretservice/integration/StaticTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/StaticTest.java @@ -1,6 +1,7 @@ package de.swiesend.secretservice.integration; import de.swiesend.secretservice.Static; +import de.swiesend.secretservice.Static.Utils; import org.junit.jupiter.api.Test; import java.math.BigInteger; @@ -32,25 +33,34 @@ public void testSecondOakleyGroup() { @Test public void isNullOrEmptyCharSeq() { CharSequence nullCharSeq = null; - assertTrue(Static.isNullOrEmpty(nullCharSeq)); + assertTrue(Utils.isNullOrEmpty(nullCharSeq)); CharSequence emptyCharSeq = ""; - assertTrue(Static.isNullOrEmpty(emptyCharSeq)); + assertTrue(Utils.isNullOrEmpty(emptyCharSeq)); CharSequence blankCharSeq = " "; - assertTrue(Static.isNullOrEmpty(blankCharSeq)); + assertTrue(Utils.isNullOrEmpty(blankCharSeq)); CharSequence nonEmptyCharSeq = "not empty"; - assertFalse(Static.isNullOrEmpty(nonEmptyCharSeq)); + assertFalse(Utils.isNullOrEmpty(nonEmptyCharSeq)); } @Test public void isNullOrEmptyStr() { String nullStr = null; - assertTrue(Static.isNullOrEmpty(nullStr)); + assertTrue(Utils.isNullOrEmpty(nullStr)); String emptyStr = ""; - assertTrue(Static.isNullOrEmpty(emptyStr)); + assertTrue(Utils.isNullOrEmpty(emptyStr)); String blankStr = " "; - assertTrue(Static.isNullOrEmpty(blankStr)); + assertTrue(Utils.isNullOrEmpty(blankStr)); String nonEmptyStr = "not empty"; - assertFalse(Static.isNullOrEmpty(nonEmptyStr)); + assertFalse(Utils.isNullOrEmpty(nonEmptyStr)); + } + + @Test + public void isNullOrEmptyArrayOfObjects() { + assertTrue(Utils.isNullOrEmpty((Object[]) null)); + Object[] emptyArrayOfObj = new Object[0]; + assertTrue(Utils.isNullOrEmpty(emptyArrayOfObj)); + Object[] nonEmptyArrayOfObj = new Object[]{new Object()}; + assertFalse(Utils.isNullOrEmpty(nonEmptyArrayOfObj)); } } diff --git a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java index 8ebb496..4804d0f 100644 --- a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java @@ -51,7 +51,7 @@ public void afterEach() throws InterruptedException { @Test public void changeWithMasterPassword() throws InterruptedException { - List collections = service.getCollections(); + List collections = service.getCollections().get(); List cs = Static.Convert.toStrings(collections); if (!cs.contains("/org/freedesktop/secrets/collection/test")) { HashMap properties = new HashMap(); @@ -83,11 +83,11 @@ public void changeWithPrompt() throws InterruptedException { @Test public void createWithMasterPassword() throws InterruptedException { - List collections = service.getCollections(); + List collections = service.getCollections().get(); List cs = Static.Convert.toStrings(collections); if (cs.contains("/org/freedesktop/secrets/collection/test")) { - ObjectPath deleted = collection.delete(); + ObjectPath deleted = collection.delete().get(); assertEquals("/", deleted.getPath()); Thread.currentThread().sleep(100L); // await signal: Service.CollectionDeleted } @@ -97,7 +97,7 @@ public void createWithMasterPassword() throws InterruptedException { iugri.createWithMasterPassword(properties, original); Thread.currentThread().sleep(100L); // await signal: Service.CollectionCreated - collections = service.getCollections(); + collections = service.getCollections().get(); cs = Static.Convert.toStrings(collections); assertTrue(cs.contains(Static.ObjectPaths.collection("test"))); diff --git a/src/test/java/de/swiesend/secretservice/integration/test/Context.java b/src/test/java/de/swiesend/secretservice/integration/test/Context.java index 01b6837..3a85a48 100644 --- a/src/test/java/de/swiesend/secretservice/integration/test/Context.java +++ b/src/test/java/de/swiesend/secretservice/integration/test/Context.java @@ -16,6 +16,7 @@ import java.util.Map; import static java.lang.System.exit; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; public class Context { @@ -74,6 +75,8 @@ public void ensureSession() { } session = service.getSession(); + log.info(session.getObjectPath()); + assertNotNull(session); assertTrue(session.getObjectPath().startsWith(Static.ObjectPaths.SESSION + "/s")); password = new Secret(session.getPath(), "".getBytes(), "test".getBytes()); @@ -84,9 +87,9 @@ public void ensureCollection() { collection = new Collection("test", service); - collections = Static.Convert.toStrings(service.getCollections()); + collections = Static.Convert.toStrings(service.getCollections().get()); if (collections.contains(Static.ObjectPaths.collection("test"))) { - ObjectPath deletePrompt = collection.delete(); + ObjectPath deletePrompt = collection.delete().get(); if (!deletePrompt.getPath().equals("/")) { log.error("won't wait for prompt in automated test context."); exit(-3); @@ -94,7 +97,7 @@ public void ensureCollection() { } Map properties = Collection.createProperties("test"); withoutPrompt.createWithMasterPassword(properties, password); - collections = Static.Convert.toStrings(service.getCollections()); + collections = Static.Convert.toStrings(service.getCollections().get()); if (collection.isLocked()) { withoutPrompt.unlockWithMasterPassword(collection.getPath(), password); @@ -128,14 +131,14 @@ public void ensureItem() { withoutPrompt.unlockWithMasterPassword(collection.getPath(), password); } - List items = collection.getItems(); + List items = collection.getItems().get(); for (ObjectPath path : items) { Item i = new Item(path, service); i.delete(); } Map properties = Item.createProperties("TestItem", attributes); - Pair response = collection.createItem(properties, secret, true); + Pair response = collection.createItem(properties, secret, true).get(); ObjectPath itemPath = response.a; item = new Item(itemPath, service); From 98b6a20ce7869433e55dbeb1d88097e05796b460 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 2 Apr 2022 15:10:36 +0200 Subject: [PATCH 06/74] Make version 2.0.0-alpha --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index af7f0eb..4be2b76 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ UTF-8 17 + 17 2.0.0 From da5ed3b5c0fea5ae807cae407813ff204414f221 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 2 Apr 2022 15:10:55 +0200 Subject: [PATCH 07/74] Add new functional interfaces --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4be2b76..80a1856 100644 --- a/pom.xml +++ b/pom.xml @@ -62,10 +62,10 @@ 2.0.0 4.3.0 - 2.0.9 + 2.0.7 - 5.10.0 + 5.9.3 From 32fee71c3d05a299c26f449cd905b0ae68f8297f Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 11 Sep 2022 16:51:44 +0200 Subject: [PATCH 08/74] Extend the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c62c1a7..aa1bc6a 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![Maven Central](https://img.shields.io/maven-central/v/de.swiesend/secret-service.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22de.swiesend%22%20AND%20a:%22secret-service%22) -A Java library for storing secrets in a keyring over the D-Bus. +A _Java_ library for storing secrets in the _Gnome Keyring_ over _DBus_. -The library is conforming to the freedesktop.org +The library is conform to the freedesktop.org [Secret Service API 0.2](https://specifications.freedesktop.org/secret-service/0.2) and thus compatible with Gnome linux systems. The Secret Service itself is implemented by the [`gnome-keyring`](https://wiki.gnome.org/action/show/Projects/GnomeKeyring) and provided by the [`gnome-keyring-daemon`](https://wiki.gnome.org/Projects/GnomeKeyring/RunningDaemon). From ca5839d50c843de4d2efef28adf5a0cf979b60ed Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 11 Sep 2022 16:53:42 +0200 Subject: [PATCH 09/74] Align --- .../simple/SimpleCollection.java | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 0c76b50..f42a05a 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -87,11 +87,11 @@ public SimpleCollection(String label, CharSequence password) throws IOException try { encrypted = transport.encrypt(password); } catch (NoSuchAlgorithmException | - NoSuchPaddingException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { + NoSuchPaddingException | + InvalidAlgorithmParameterException | + InvalidKeyException | + BadPaddingException | + IllegalBlockSizeException e) { log.error("Could not establish transport encryption.", e); } } @@ -290,10 +290,10 @@ private void init() throws IOException { withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); } } catch (NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeySpecException | - InvalidKeyException | - DBusException e) { + InvalidAlgorithmParameterException | + InvalidKeySpecException | + InvalidKeyException | + DBusException e) { throw new IOException("Could not initiate transport encryption.", e); } } @@ -381,7 +381,7 @@ private void unlock() { performPrompt(response.b); if (!collection.isLocked()) { isUnlockedOnceWithUserPermission = true; - log.info("Unlocked collection: " + collection.getLabel() + " (" + collection.getObjectPath() + ")"); + log.info("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); } } } @@ -487,11 +487,11 @@ public String createItem(String label, CharSequence password, Map new Secret(sessionPath, null))) { decrypted = transport.decrypt(secret); } catch (NoSuchPaddingException | - NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { + NoSuchAlgorithmException | + InvalidAlgorithmParameterException | + InvalidKeyException | + BadPaddingException | + IllegalBlockSizeException e) { log.error("Could not decrypt the secret.", e); } return decrypted; From 9fc24291721f6a1096170c0d6b1e4adf309a980c Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 11 Sep 2022 16:54:05 +0200 Subject: [PATCH 10/74] Only log simple names --- .../secretservice/handlers/SignalHandler.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java index 93055ce..8e4d265 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java @@ -65,27 +65,27 @@ public void handle(DBusSignal s) { if (s instanceof Collection.ItemCreated) { Collection.ItemCreated ic = (Collection.ItemCreated) s; - log.info("Received signal Collection.ItemCreated: " + ic.item); + log.info("Received signal: Collection.ItemCreated(" + ic.item + ")"); } else if (s instanceof Collection.ItemChanged) { Collection.ItemChanged ic = (Collection.ItemChanged) s; - log.debug("Received signal Collection.ItemChanged: " + ic.item); + log.debug("Received signal: Collection.ItemChanged(" + ic.item + ")"); } else if (s instanceof Collection.ItemDeleted) { Collection.ItemDeleted ic = (Collection.ItemDeleted) s; - log.info("Received signal Collection.ItemDeleted: " + ic.item); + log.info("Received signal: Collection.ItemDeleted(" + ic.item + ")"); } else if (s instanceof Prompt.Completed) { Prompt.Completed c = (Prompt.Completed) s; - log.info("Received signal Prompt.Completed(" + s.getPath() + "): {dismissed: " + c.dismissed + ", result: " + c.result + "}"); + log.info("Received signal: Prompt.Completed(" + s.getPath() + "): {dismissed: " + c.dismissed + ", result: " + c.result + "}"); } else if (s instanceof Service.CollectionCreated) { Service.CollectionCreated cc = (Service.CollectionCreated) s; - log.info("Received signal Service.CollectionCreated: " + cc.collection); + log.info("Received signal: Service.CollectionCreated(" + cc.collection + ")"); } else if (s instanceof Service.CollectionChanged) { Service.CollectionChanged cc = (Service.CollectionChanged) s; - log.info("Received signal Service.CollectionChanged: " + cc.collection); + log.info("Received signal: Service.CollectionChanged(" + cc.collection + ")"); } else if (s instanceof Service.CollectionDeleted) { Service.CollectionDeleted cc = (Service.CollectionDeleted) s; - log.info("Received signal Service.CollectionDeleted: " + cc.collection); + log.info("Received signal: Service.CollectionDeleted(" + cc.collection + ")"); } else try { - log.warn("Received unexpected signal: " + s.getClass().toString() + " {" + s.toString() + "}"); + log.warn("Received unexpected signal: " + s.getClass().getName() + ": {" + s + "}"); } catch (NullPointerException e) { log.warn("Received unknown signal."); } From dfb60d1b796c5a9f7a7b34d1dd9ce7e2031f145e Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 11 Sep 2022 16:55:49 +0200 Subject: [PATCH 11/74] Enable setProperty() with checked response --- .../handlers/MessageHandler.java | 37 ++++++++----------- .../secretservice/handlers/Messaging.java | 2 +- .../secretservice/integration/ItemTest.java | 4 +- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 32d2166..deea436 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -24,6 +24,11 @@ public MessageHandler(DBusConnection connection) { this.connection = connection; } + public MessageHandler(DBusConnection connection, boolean fireAndForget) { + this.connection = connection; + this.fireAndForget = fireAndForget; + } + public Optional send(String service, String path, String iface, String method, String signature, Object... args) { try { org.freedesktop.dbus.messages.Message message = new MethodCall( @@ -35,12 +40,13 @@ public Optional send(String service, String path, String iface, String connection.sendMessage(message); org.freedesktop.dbus.messages.Message response = ((MethodCall) message).getReply(MAX_DELAY_MILLIS); - if (log.isTraceEnabled()) log.trace(String.valueOf(response)); + if (log.isTraceEnabled()) log.trace("Response: " + response); Object[] parameters = null; if (response != null) { parameters = response.getParameters(); - if (log.isDebugEnabled()) log.debug(Arrays.deepToString(parameters)); + if (log.isDebugEnabled()) + log.debug("Response parameters for method " + iface + "/" + method + ": " + Arrays.deepToString(parameters)); } if (response instanceof org.freedesktop.dbus.errors.Error) { @@ -71,7 +77,7 @@ public Optional send(String service, String path, String iface, String if (log.isDebugEnabled()) log.debug(error); return Optional.empty(); default: - log.error("Unexpected org.freedesktop.dbus.errors.Error: ", error); + log.error("Unexpected org.freedesktop.dbus.errors.Error: \"" + error + "\" with parameters: " + Arrays.deepToString(parameters)); return Optional.empty(); } } @@ -101,25 +107,14 @@ public Optional getAllProperties(String service, String path, String if } public boolean setProperty(String service, String path, String iface, String property, Variant value) { + if (log.isDebugEnabled()) log.debug(iface + "@" + property + " with variant: " + value); Optional maybeResponse = send(service, path, Static.DBus.Interfaces.DBUS_PROPERTIES, "Set", "ssv", iface, property, value); - // TODO: resolve return value -// if (maybeResponse.isPresent() && !fireAndForget) { -// Optional maybeValue = getProperty(service, path, iface, property); -// if (maybeValue.isPresent()) { -// Variant result = maybeValue.get(); -// if (result == null) return false; -// boolean valueCond = value.getValue() == result.getValue(); -// boolean signatureCond = value.getSig() == result.getSig(); -// boolean typeCond = value.getType() == result.getType(); -// return valueCond && signatureCond && typeCond; -// } else { -// return false; -// } -// } else { -// return maybeResponse.isPresent(); -// } - //return true; - return maybeResponse.isPresent(); + if (maybeResponse.isPresent() && !fireAndForget) { + Optional maybePropertyValue = getProperty(service, path, iface, property); + return value.equals(maybePropertyValue.orElse(null)); + } else { + return maybeResponse.isPresent(); + } } } diff --git a/src/main/java/de/swiesend/secretservice/handlers/Messaging.java b/src/main/java/de/swiesend/secretservice/handlers/Messaging.java index 1cf5fb9..f9364c0 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/Messaging.java +++ b/src/main/java/de/swiesend/secretservice/handlers/Messaging.java @@ -21,7 +21,7 @@ public abstract class Messaging { public Messaging(DBusConnection connection, List> signals, String serviceName, String objectPath, String interfaceName) { this.connection = connection; - this.msg = new MessageHandler(connection); + this.msg = new MessageHandler(connection, true); if (signals != null) { this.sh.connect(connection, signals); } diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index 9a6924e..7451c54 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -16,6 +16,7 @@ import static de.swiesend.secretservice.integration.test.Context.label; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; public class ItemTest { @@ -165,7 +166,8 @@ public void getLabel() { @Test public void setLabel() { - context.item.setLabel("RelabeledItem"); + boolean result = context.item.setLabel("RelabeledItem"); + assertTrue(result); String label = context.item.getLabel().get(); log.info(label("label", label)); assertEquals("RelabeledItem", label); From e23b3590ce6e65568c3287b795b8a6cf9424fcc4 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 6 Oct 2022 14:10:45 +0200 Subject: [PATCH 12/74] Add "org.freedesktop.DBus.Error.UnknownObject" --- .../de/swiesend/secretservice/handlers/MessageHandler.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index deea436..322d915 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -69,11 +69,12 @@ public Optional send(String service, String path, String iface, String } return Optional.empty(); case "org.freedesktop.DBus.Error.NoReply": - case "org.freedesktop.DBus.Error.UnknownMethod": case "org.freedesktop.DBus.Error.ServiceUnknown": - case "org.freedesktop.dbus.exceptions.NotConnected": + case "org.freedesktop.DBus.Error.UnknownMethod": + case "org.freedesktop.DBus.Error.UnknownObject": case "org.freedesktop.DBus.Local.Disconnected": case "org.freedesktop.dbus.exceptions.FatalDBusException": + case "org.freedesktop.dbus.exceptions.NotConnected": if (log.isDebugEnabled()) log.debug(error); return Optional.empty(); default: From 19cb9aa1634f631dae0a6b9246c12f672293e1fd Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 10 Nov 2022 11:26:22 +0100 Subject: [PATCH 13/74] Add subclasses for unambiguous control flow for the initialization --- .../secretservice/TransportEncryption.java | 257 ++++++++++-------- .../simple/SimpleCollection.java | 66 ++--- .../integration/IntegrationTest.java | 23 +- .../integration/test/Context.java | 14 +- 4 files changed, 195 insertions(+), 165 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index 388c46b..240e969 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -6,6 +6,8 @@ import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.types.Variant; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.crypto.*; import javax.crypto.interfaces.DHPublicKey; @@ -25,7 +27,8 @@ public class TransportEncryption implements AutoCloseable { public static final int PRIVATE_VALUE_BITS = 1024; public static final int AES_BITS = 128; - private Service service; + private static final Logger log = LoggerFactory.getLogger(TransportEncryption.class); + private de.swiesend.secretservice.Service service; // TODO: should adhere to the interface private DHParameterSpec dhParameters = null; private KeyPair keypair = null; private PublicKey publicKey = null; @@ -42,7 +45,7 @@ public TransportEncryption(DBusConnection connection) { this.service = new Service(connection); } - public TransportEncryption(Service service) { + public TransportEncryption(de.swiesend.secretservice.Service service) { this.service = service; } @@ -54,145 +57,167 @@ static private int toBytes(int bits) { return bits / 8; } - public void initialize() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException { + public Optional initialize() { - // create dh parameter specification with prime, generator and bits - BigInteger prime = fromBinary(Static.RFC_2409.SecondOakleyGroup.PRIME); - BigInteger generator = fromBinary(Static.RFC_2409.SecondOakleyGroup.GENERATOR); - dhParameters = new DHParameterSpec(prime, generator, PRIVATE_VALUE_BITS); + try { + // create dh parameter specification with prime, generator and bits + BigInteger prime = fromBinary(Static.RFC_2409.SecondOakleyGroup.PRIME); + BigInteger generator = fromBinary(Static.RFC_2409.SecondOakleyGroup.GENERATOR); + dhParameters = new DHParameterSpec(prime, generator, PRIVATE_VALUE_BITS); + + // generate DH keys from specification + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(Static.Algorithm.DIFFIE_HELLMAN); + keyPairGenerator.initialize(dhParameters); + keypair = keyPairGenerator.generateKeyPair(); + publicKey = keypair.getPublic(); + privateKey = keypair.getPrivate(); + + return Optional.of(new InitializedSession()); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + log.error("The secret service could not be initialized as the service does not provide the expected transport encryption algorithm.", e); + return Optional.empty(); + } + } - // generate DH keys from specification - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(Static.Algorithm.DIFFIE_HELLMAN); - keyPairGenerator.initialize(dhParameters); - keypair = keyPairGenerator.generateKeyPair(); - publicKey = keypair.getPublic(); - privateKey = keypair.getPrivate(); + public Service getService() { + return service; } - public boolean openSession() throws DBusException { - if (keypair == null) { - throw new IllegalStateException("Missing own keypair. Call initialize() first."); + public void clear() { + if (privateKey != null) try { + privateKey.destroy(); + } catch (DestroyFailedException e) { + Secret.clear(privateKey.getEncoded()); } - - // The public keys are transferred as an array of bytes representing an unsigned integer of arbitrary size, - // most-significant byte first (e.g., the integer 32768 is represented as the 2-byte string 0x80 0x00) - BigInteger ya = ((DHPublicKey) publicKey).getY(); - - // open session with "Client DH pub key as an array of bytes" without prime or generator - Optional, ObjectPath>> osResponse = service.openSession( - Static.Algorithm.DH_IETF1024_SHA256_AES128_CBC_PKCS7, new Variant(ya.toByteArray())); - - // transform peer's raw Y to a public key - if (osResponse.isPresent()) { - yb = osResponse.get().a.getValue(); - return true; - } else { - return false; + if (sessionKey != null) try { + sessionKey.destroy(); + } catch (DestroyFailedException e) { + Secret.clear(sessionKey.getEncoded()); } } - public void generateSessionKey() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { - if (yb == null) { - throw new IllegalStateException("Missing peer public key. Call openSession() first."); - } - - DHPublicKeySpec dhPublicKeySpec = new DHPublicKeySpec(fromBinary(yb), dhParameters.getP(), dhParameters.getG()); - KeyFactory keyFactory = KeyFactory.getInstance(Static.Algorithm.DIFFIE_HELLMAN); - DHPublicKey peerPublicKey = (DHPublicKey) keyFactory.generatePublic(dhPublicKeySpec); - - KeyAgreement keyAgreement = KeyAgreement.getInstance(Static.Algorithm.DIFFIE_HELLMAN); - keyAgreement.init(privateKey); - keyAgreement.doPhase(peerPublicKey, true); - byte[] rawSessionKey = keyAgreement.generateSecret(); - - // HKDF digest into a 128-bit key by extract and expand with "NULL salt and empty info" - // see: https://standards.freedesktop.org/secret-service/0.2/ch07s03.html - byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract((byte[]) null, rawSessionKey); - byte[] keyingMaterial = HKDF.fromHmacSha256().expand(pseudoRandomKey, null, toBytes(AES_BITS)); - - sessionKey = new SecretKeySpec(keyingMaterial, Static.Algorithm.AES); + @Override + public void close() { + clear(); } - public Secret encrypt(CharSequence plain) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - - final byte[] bytes = Secret.toBytes(plain); - try { - return encrypt(bytes, StandardCharsets.UTF_8); - } finally { - Secret.clear(bytes); + public class InitializedSession { + + public Optional openSession() { + if (keypair == null) { + throw new IllegalStateException("Missing own keypair. Call TransportEncryption.initialize() first."); + } + // The public keys are transferred as an array of bytes representing an unsigned integer of arbitrary size, + // most-significant byte first (e.g., the integer 32768 is represented as the 2-byte string 0x80 0x00) + BigInteger ya = ((DHPublicKey) publicKey).getY(); + + // open session with "Client DH pub key as an array of bytes" without prime or generator + return service.openSession( + Static.Algorithm.DH_IETF1024_SHA256_AES128_CBC_PKCS7, + new Variant(ya.toByteArray()) + ).flatMap(pair -> { + // transform peer's raw Y to a public key + yb = pair.a.getValue(); + return Optional.of(new OpenedSession()); + }); } } - public Secret encrypt(byte[] plain, Charset charset) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - - if (plain == null) return null; - - if (service == null) { - throw new IllegalStateException("Missing session. Call openSession() first."); - } - if (sessionKey == null) { - throw new IllegalStateException("Missing session key. Call generateSessionKey() first."); + public class OpenedSession { + public Optional generateSessionKey() { + if (yb == null) { + throw new IllegalStateException("Missing peer public key. Call Initialized.openSession() first."); + } + try { + DHPublicKeySpec dhPublicKeySpec = new DHPublicKeySpec(fromBinary(yb), dhParameters.getP(), dhParameters.getG()); + KeyFactory keyFactory = KeyFactory.getInstance(Static.Algorithm.DIFFIE_HELLMAN); + DHPublicKey peerPublicKey = (DHPublicKey) keyFactory.generatePublic(dhPublicKeySpec); + + KeyAgreement keyAgreement = KeyAgreement.getInstance(Static.Algorithm.DIFFIE_HELLMAN); + keyAgreement.init(privateKey); + keyAgreement.doPhase(peerPublicKey, true); + byte[] rawSessionKey = keyAgreement.generateSecret(); + + // HKDF digest into a 128-bit key by extract and expand with "NULL salt and empty info" + // see: https://standards.freedesktop.org/secret-service/0.2/ch07s03.html + byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract((byte[]) null, rawSessionKey); + byte[] keyingMaterial = HKDF.fromHmacSha256().expand(pseudoRandomKey, null, toBytes(AES_BITS)); + + sessionKey = new SecretKeySpec(keyingMaterial, Static.Algorithm.AES); + + if (sessionKey != null) { + return Optional.of(new EncryptedSession()); + } else { + return Optional.empty(); + } + } catch (NoSuchAlgorithmException | InvalidKeySpecException | InvalidKeyException e) { + log.error("Could not generate a new session key for the current session", e); + return Optional.empty(); + } } - // secret.parameter - 16 byte AES initialization vector - final byte[] salt = new byte[toBytes(AES_BITS)]; - SecureRandom random = SecureRandom.getInstance(Static.Algorithm.SHA1_PRNG); - random.nextBytes(salt); - IvParameterSpec ivSpec = new IvParameterSpec(salt); + } - Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); - cipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivSpec); + public class EncryptedSession { + public Secret encrypt(CharSequence plain) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - String contentType = Secret.createContentType(charset); + final byte[] bytes = Secret.toBytes(plain); + try { + return encrypt(bytes, StandardCharsets.UTF_8); + } finally { + Secret.clear(bytes); + } + } - return new Secret(service.getSession().getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType); - } + public Secret encrypt(byte[] plain, Charset charset) throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - public char[] decrypt(Secret secret) throws NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidAlgorithmParameterException, - InvalidKeyException, - BadPaddingException, - IllegalBlockSizeException { + if (plain == null) return null; - if (secret == null) return null; + if (service == null) { + throw new IllegalStateException("Missing session. Call Initialized.openSession() first."); + } + if (sessionKey == null) { + throw new IllegalStateException("Missing session key. Call Opened.generateSessionKey() first."); + } - if (sessionKey == null) { - throw new IllegalStateException("Missing session key. Call generateSessionKey() first."); - } + // secret.parameter - 16 byte AES initialization vector + final byte[] salt = new byte[toBytes(AES_BITS)]; + SecureRandom random = SecureRandom.getInstance(Static.Algorithm.SHA1_PRNG); + random.nextBytes(salt); + IvParameterSpec ivSpec = new IvParameterSpec(salt); - IvParameterSpec ivSpec = new IvParameterSpec(secret.getSecretParameters()); - Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); - cipher.init(Cipher.DECRYPT_MODE, sessionKey, ivSpec); - final byte[] decrypted = cipher.doFinal(secret.getSecretValue()); - try { - return Secret.toChars(decrypted); - } finally { - Secret.clear(decrypted); - } - } + Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); + cipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivSpec); - public Service getService() { - return service; - } + String contentType = Secret.createContentType(charset); - public void clear() { - if (privateKey != null) try { - privateKey.destroy(); - } catch (DestroyFailedException e) { - Secret.clear(privateKey.getEncoded()); + return new Secret(service.getSession().getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType); } - if (sessionKey != null) try { - sessionKey.destroy(); - } catch (DestroyFailedException e) { - Secret.clear(sessionKey.getEncoded()); - } - } - @Override - public void close() { - clear(); + public char[] decrypt(Secret secret) throws NoSuchPaddingException, + NoSuchAlgorithmException, + InvalidAlgorithmParameterException, + InvalidKeyException, + BadPaddingException, + IllegalBlockSizeException { + + if (secret == null) return null; + + if (sessionKey == null) { + throw new IllegalStateException("Missing session key. Call Opened.generateSessionKey() first."); + } + + IvParameterSpec ivSpec = new IvParameterSpec(secret.getSecretParameters()); + Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); + cipher.init(Cipher.DECRYPT_MODE, sessionKey, ivSpec); + final byte[] decrypted = cipher.doFinal(secret.getSecretValue()); + try { + return Secret.toChars(decrypted); + } finally { + Secret.clear(decrypted); + } + } } } diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index f42a05a..91364b9 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -8,6 +8,7 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.types.Variant; +import de.swiesend.secretservice.Collection; import de.swiesend.secretservice.interfaces.Prompt.Completed; import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.slf4j.Logger; @@ -21,12 +22,8 @@ import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; import java.time.Duration; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.RejectedExecutionException; import static de.swiesend.secretservice.Static.DBus.DEFAULT_DELAY_MILLIS; @@ -39,6 +36,7 @@ public final class SimpleCollection extends de.swiesend.secretservice.simple.int private static final Logger log = LoggerFactory.getLogger(SimpleCollection.class); private static final DBusConnection connection = getConnection(); private static final Thread shutdownHook = setupShutdownHook(); + private TransportEncryption.EncryptedSession transportEncryptedSession = null; private TransportEncryption transport = null; private Service service = null; private Session session = null; @@ -85,7 +83,7 @@ public SimpleCollection(String label, CharSequence password) throws IOException init(); if (password != null) { try { - encrypted = transport.encrypt(password); + encrypted = transportEncryptedSession.encrypt(password); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | @@ -190,18 +188,20 @@ public static boolean isAvailable() { // algorithm (DH_IETF1024_SHA256_AES128_CBC_PKCS7) or raises an error, like // "org.freedesktop.DBus.Error.ServiceUnknown <: org.freedesktop.dbus.exceptions.DBusException" TransportEncryption transport = new TransportEncryption(connection); - transport.initialize(); - boolean isSessionSupported = transport.openSession(); + Optional opened = transport.initialize().flatMap(init -> init.openSession()); + boolean isSessionSupported = opened.isPresent(); transport.close(); return isSessionSupported; } catch (DBusException | ExceptionInInitializerError e) { log.warn("The secret service is not available. You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); return false; - } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { + } + // TODO: remove comment + /*catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { log.error("The secret service could not be initialized as the service does not provide the expected transport encryption algorithm.", e); return false; - } + }*/ } else { log.error("No D-Bus connection: Cannot check if all needed services are available."); return false; @@ -278,24 +278,18 @@ private static Thread setupShutdownHook() { private void init() throws IOException { if (!isAvailable()) throw new IOException("The secret service is not available."); - try { - transport = new TransportEncryption(connection); - transport.initialize(); - transport.openSession(); - transport.generateSessionKey(); - service = transport.getService(); - session = service.getSession(); - prompt = new Prompt(service); - if (isGnomeKeyringAvailable()) { - withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); - } - } catch (NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeySpecException | - InvalidKeyException | - DBusException e) { - throw new IOException("Could not initiate transport encryption.", e); - } + TransportEncryption transport = new TransportEncryption(connection); + transportEncryptedSession = transport + .initialize() + .flatMap(i -> i.openSession()) + .flatMap(o -> o.generateSessionKey()) + .orElseThrow( + () -> new IOException("Could not initiate transport encryption.") + ); + service = transport.getService(); + session = service.getSession(); + prompt = new Prompt(service); + withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); } private Map getLabels() { @@ -362,7 +356,7 @@ private List lockable() { public void lock() { if (collection != null && !collection.isLocked()) { service.lock(lockable()); - log.info("Locked collection: " + collection.getLabel() + " (" + collection.getObjectPath() + ")"); + log.info("Locked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); try { Thread.currentThread().sleep(DEFAULT_DELAY_MILLIS); } catch (InterruptedException e) { @@ -373,16 +367,16 @@ public void lock() { private void unlock() { if (collection != null && collection.isLocked()) { - if (withoutPrompt != null && encrypted != null) { - withoutPrompt.unlockWithMasterPassword(collection.getPath(), encrypted); - log.debug("Unlocked collection: " + collection.getLabel() + " (" + collection.getObjectPath() + ")"); - } else { + if (encrypted == null || isDefault()) { Pair, ObjectPath> response = service.unlock(lockable()).get(); performPrompt(response.b); if (!collection.isLocked()) { isUnlockedOnceWithUserPermission = true; log.info("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); } + } else { + withoutPrompt.unlockWithMasterPassword(collection.getPath(), encrypted); + log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); } } } @@ -475,7 +469,7 @@ public String createItem(String label, CharSequence password, Map properties = Item.createProperties(label, attributes); - try (final Secret secret = transport.encrypt(password)) { + try (final Secret secret = transportEncryptedSession.encrypt(password)) { Pair response = collection.createItem(properties, secret, false).get(); if (response == null) return null; item = response.a; @@ -543,7 +537,7 @@ public void updateItem(String objectPath, String label, CharSequence password, M item.setAttributes(attributes); } - if (password != null) try (Secret secret = transport.encrypt(password)) { + if (password != null) try (Secret secret = transportEncryptedSession.encrypt(password)) { item.setSecret(secret); } catch (NoSuchAlgorithmException | NoSuchPaddingException | @@ -619,7 +613,7 @@ public char[] getSecret(String objectPath) { char[] decrypted = null; ObjectPath sessionPath = session.getPath(); try (final Secret secret = item.getSecret(sessionPath).orElseGet(() -> new Secret(sessionPath, null))) { - decrypted = transport.decrypt(secret); + decrypted = transportEncryptedSession.decrypt(secret); } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | diff --git a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java index 3890f46..2457332 100644 --- a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java @@ -13,6 +13,7 @@ import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.security.auth.DestroyFailedException; +import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -37,20 +38,26 @@ public void testWithTransportEncryption() throws NoSuchPaddingException, InvalidKeyException, BadPaddingException, - IllegalBlockSizeException, InterruptedException, InvalidKeySpecException, DestroyFailedException { + IllegalBlockSizeException, + InterruptedException, + IOException { TransportEncryption transportEncryption = new TransportEncryption(); - transportEncryption.initialize(); - transportEncryption.openSession(); - transportEncryption.generateSessionKey(); + TransportEncryption.EncryptedSession encryptedSession = transportEncryption + .initialize() + .flatMap(i -> i.openSession()) + .flatMap(o -> o.generateSessionKey()) + .orElseThrow( + () -> new IOException("Could not initiate transport encryption.") + ); String plain = "super secret"; - Secret encrypted = transportEncryption.encrypt(plain); + Secret encrypted = encryptedSession.encrypt(plain); byte[] encBase64 = Base64.getEncoder().encode(encrypted.getSecretValue()); log.info(label("encrypted secret (base64)", new String(encBase64))); - char[] decrypted = transportEncryption.decrypt(encrypted); + char[] decrypted = encryptedSession.decrypt(encrypted); log.info(label(" decrypted secret", new String(decrypted))); assertEquals(plain, new String(decrypted)); @@ -58,7 +65,7 @@ public void testWithTransportEncryption() throws Session session = service.getSession(); InternalUnsupportedGuiltRiddenInterface noPrompt = new InternalUnsupportedGuiltRiddenInterface(service); - Secret master = transportEncryption.encrypt("test"); + Secret master = encryptedSession.encrypt("test"); Collection collection = new Collection("test", service); List collections = Static.Convert.toStrings(service.getCollections().get()); @@ -97,7 +104,7 @@ public void testWithTransportEncryption() throws assertEquals(encrypted.getSession(), actual.getSession()); assertEquals(encrypted.getContentType(), actual.getContentType()); - decrypted = transportEncryption.decrypt(actual); + decrypted = encryptedSession.decrypt(actual); log.info(label(" decrypted remote secret", new String(decrypted))); assertEquals(plain, new String(decrypted)); diff --git a/src/test/java/de/swiesend/secretservice/integration/test/Context.java b/src/test/java/de/swiesend/secretservice/integration/test/Context.java index 3a85a48..8d845a2 100644 --- a/src/test/java/de/swiesend/secretservice/integration/test/Context.java +++ b/src/test/java/de/swiesend/secretservice/integration/test/Context.java @@ -9,6 +9,7 @@ import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.slf4j.Logger; +import java.io.IOException; import java.math.BigInteger; import java.util.Arrays; import java.util.HashMap; @@ -24,7 +25,7 @@ public class Context { public Logger log; public boolean encrypted; - public TransportEncryption encryption = null; + public TransportEncryption.EncryptedSession encryption = null; public Service service = null; public Session session = null; public Secret password = null; @@ -62,10 +63,13 @@ public void ensureSession() { try { if (encrypted) { - encryption = new TransportEncryption(service); - encryption.initialize(); - encryption.openSession(); - encryption.generateSessionKey(); + encryption = new TransportEncryption(service) + .initialize() + .flatMap(i -> i.openSession()) + .flatMap(o -> o.generateSessionKey()) + .orElseThrow( + () -> new IOException("Could not initiate transport encryption.") + ); } else { service.openSession(Static.Algorithm.PLAIN, new Variant("")); } From 252a2d88dc69c4b17aeec976e78f3f166d865b47 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 10 Nov 2022 11:32:23 +0100 Subject: [PATCH 14/74] Shorten type --- src/main/java/de/swiesend/secretservice/Service.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/Service.java b/src/main/java/de/swiesend/secretservice/Service.java index 903dc5b..288b6fd 100644 --- a/src/main/java/de/swiesend/secretservice/Service.java +++ b/src/main/java/de/swiesend/secretservice/Service.java @@ -26,7 +26,7 @@ public Service(DBusConnection connection) { public Optional, ObjectPath>> openSession(String algorithm, Variant input) { return send("OpenSession", "sv", algorithm, input) .filter(response -> !Static.Utils.isNullOrEmpty(response) && response.length == 2) - .flatMap(response -> Optional.of(new Pair, ObjectPath>((Variant) response[0], (ObjectPath) response[1]))) + .flatMap(response -> Optional.of(new Pair<>((Variant) response[0], (ObjectPath) response[1]))) .map(pair -> { log.debug("Got session: " + pair.b.getPath()); session = new Session(pair.b, this); From 4e014ddc0a9b5c6727c0ff9f96f91237e71b923f Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 10 Nov 2022 11:33:35 +0100 Subject: [PATCH 15/74] Implement the new secret service 2.0 --- .../secret/functional/SecretService.java | 199 ++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 src/main/java/de/swiesend/secret/functional/SecretService.java diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java new file mode 100644 index 0000000..6183729 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -0,0 +1,199 @@ +package de.swiesend.secret.functional; + +import de.swiesend.secret.functional.interfaces.ServiceInterface; +import de.swiesend.secret.functional.interfaces.SessionInterface; +import de.swiesend.secret.functional.interfaces.SystemInterface; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBus; +import org.freedesktop.secret.Pair; +import org.freedesktop.secret.Service; +import org.freedesktop.secret.Static; +import org.freedesktop.secret.TransportEncryption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.*; + + +enum Activatable { + DBUS(Static.DBus.Service.DBUS), + SECRETS(Static.Service.SECRETS), + GNOME_KEYRING(org.gnome.keyring.Static.Service.KEYRING); + + public final String name; + + Activatable(String name) { + this.name = name; + } + +} + +class AvailableServices { + + private static final Logger log = LoggerFactory.getLogger(AvailableServices.class); + + public EnumSet services = EnumSet.noneOf(Activatable.class); + + public AvailableServices(System system) { + DBusConnection connection = system.getConnection(); + if (connection.isConnected()) { + try { + DBus bus = connection.getRemoteObject( + Static.DBus.Service.DBUS, + Static.DBus.ObjectPaths.DBUS, + DBus.class); + List activatableServices = Arrays.asList(bus.ListActivatableNames()); + + if (!activatableServices.contains(Static.DBus.Service.DBUS)) { + log.error("Missing D-Bus service: " + Static.DBus.Service.DBUS); + } else { + services.add(Activatable.DBUS); + } + + if (!activatableServices.contains(Static.Service.SECRETS)) { + log.error("Missing D-Bus service: " + Static.Service.SECRETS); + } else { + services.add(Activatable.SECRETS); + } + if (!activatableServices.contains(org.gnome.keyring.Static.Service.KEYRING)) { + log.warn("Proceeding without D-Bus service: " + org.gnome.keyring.Static.Service.KEYRING); + } else { + services.add(Activatable.GNOME_KEYRING); + } + } catch (DBusException | ExceptionInInitializerError e) { + log.warn("The secret service is not available. You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); + } + } + } + + +} + +public class SecretService extends ServiceInterface { + + private static final Logger log = LoggerFactory.getLogger(SecretService.class); + private Map sessions = new HashMap<>(); + private org.freedesktop.secret.Service service = null; + + // TODO: remove unnecessary fields + // private Prompt prompt = null; + // private InternalUnsupportedGuiltRiddenInterface withoutPrompt = null; + // private org.freedesktop.secret.Session session = null; + private boolean isOrgGnomeKeyringAvailable = false; + + private SecretService(SystemInterface system, AvailableServices available) { + this.service = new Service(system.getConnection()); + this.isOrgGnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); + } + + /** + * Create a Secret-Service instance with initialized transport encryption. + */ + public static Optional create() { + return System.connect() + .map(system -> new Pair<>(system, new AvailableServices(system))) + .filter(pair -> isAvailable(pair.a, pair.b)) + .map(pair -> new SecretService(pair.a, pair.b)); + } + + /** + * Checks if all necessary D-Bus services are provided by the system:
+ * org.freedesktop.DBus
+ * org.freedesktop.secrets
+ * org.gnome.keyring + * + * @return true if the secret service is available, otherwise false and will log an error message. + */ + private static boolean isAvailable(System system, AvailableServices available) { + DBusConnection connection = system.getConnection(); + if (connection.isConnected()) { + try { + if (!available.services.contains(Activatable.DBUS)) { + log.error("Missing D-Bus service: " + Activatable.DBUS.name); + return false; + } + if (!available.services.contains(Activatable.SECRETS)) { + log.error("Missing D-Bus service: " + Activatable.SECRETS.name); + return false; + } + if (!available.services.contains(Activatable.GNOME_KEYRING)) { + log.warn("Proceeding without D-Bus service: " + Activatable.GNOME_KEYRING.name); + } + + // The following calls intent to open a session without actually generating a full session. + // Necessary in order to check if the provided 'secret service' supports the expected transport + // encryption algorithm (DH_IETF1024_SHA256_AES128_CBC_PKCS7) or raises an error, like + // "org.freedesktop.DBus.Error.ServiceUnknown <: org.freedesktop.dbus.exceptions.DBusException" + TransportEncryption transport = new TransportEncryption(connection); + boolean isSessionSupported = transport + .initialize() + .flatMap(init -> init.openSession()) + .isPresent(); + transport.close(); + + return isSessionSupported; + } catch (ExceptionInInitializerError e) { + log.warn("The secret service is not available. " + + "You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); + return false; + } + } else { + log.error("No D-Bus connection: Cannot check if all needed services are available."); + return false; + } + } + + @Override + public boolean isOrgGnomeKeyringAvailable() { + return isOrgGnomeKeyringAvailable; + } + + @Override + public boolean clear() { + return false; + } + + @Override + public Optional openSession() { + return Session.open(this); + } + + public void registerSession(Session session) { + this.sessions.put(session.getId(), session); + } + + public void unregisterSession(Session session) { + this.sessions.remove(session.getId()); + } + + @Override + public List getSessions() { + return this.sessions.values().stream().toList(); + } + + /*@Override + public SystemInterface getSystem() { + return this.system; + }*/ + + @Override + public Duration getTimeout() { + return null; + } + + @Override + public void setTimeout(Duration timeout) { + + } + + @Override + public void close() throws Exception { + + } + + public Service getService() { + return service; + } +} From 9d02040ffc4d47f9208bc7dd41ce0dca1b60e87c Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 10 Nov 2022 11:35:30 +0100 Subject: [PATCH 16/74] Add interfaces and implementations --- .../secret/functional/Collection.java | 298 ++++++++++++++++++ .../swiesend/secret/functional/Session.java | 91 ++++++ .../de/swiesend/secret/functional/System.java | 66 ++++ .../interfaces/SystemInterface.java | 26 ++ .../secret/functional/SecretServiceTest.java | 34 ++ 5 files changed, 515 insertions(+) create mode 100644 src/main/java/de/swiesend/secret/functional/Collection.java create mode 100644 src/main/java/de/swiesend/secret/functional/Session.java create mode 100644 src/main/java/de/swiesend/secret/functional/System.java create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java create mode 100644 src/test/java/de/swiesend/secret/functional/SecretServiceTest.java diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java new file mode 100644 index 0000000..f4706a7 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -0,0 +1,298 @@ +package de.swiesend.secret.functional; + +import de.swiesend.secret.functional.interfaces.CollectionInterface; +import de.swiesend.secret.functional.interfaces.ServiceInterface; +import de.swiesend.secret.functional.interfaces.SessionInterface; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.ObjectPath; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.types.Variant; +import org.freedesktop.secret.*; +import org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; +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.time.Duration; +import java.util.*; + +import static org.freedesktop.secret.Static.DEFAULT_PROMPT_TIMEOUT; + +public class Collection implements CollectionInterface { + + private static final Logger log = LoggerFactory.getLogger(Collection.class); + /*this.prompt = new Prompt(service); + this.withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); + */ + org.freedesktop.secret.Collection collection = null; + SessionInterface session = null; + ServiceInterface service = null; + DBusConnection connection = null; + private Duration timeout = DEFAULT_PROMPT_TIMEOUT; + private Boolean isUnlockedOnceWithUserPermission = false; + private String label = null; + private Secret encrypted = null; + private Prompt prompt = null; + + public Collection(SessionInterface session, String label, CharSequence password) { + init(session); + this.label = label; + try { + this.encrypted = session.getEncryptedSession().encrypt(password); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (NoSuchPaddingException e) { + throw new RuntimeException(e); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } catch (InvalidKeyException e) { + throw new RuntimeException(e); + } catch (BadPaddingException e) { + throw new RuntimeException(e); + } catch (IllegalBlockSizeException e) { + throw new RuntimeException(e); + } + } + + public Collection(SessionInterface session) { + init(session); + } + + private void init(SessionInterface session) { + this.session = session; + ObjectPath path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); + collection = new org.freedesktop.secret.Collection(path, session.getService().getService()); + prompt = new Prompt(session.getService().getService()); + log.info(collection.getId()); + this.service = session.getService(); + this.connection = service.getService().getConnection(); + } + + @Override + public boolean clear() { + return false; + } + + @Override + public Optional createItem(String label, CharSequence password) { + return Optional.empty(); + } + + @Override + public Optional createItem(String label, CharSequence password, Map attributes) { + if (Static.Utils.isNullOrEmpty(password)) { + throw new IllegalArgumentException("The password may not be null or empty."); + } + if (label == null) { + throw new IllegalArgumentException("The label of the item may not be null."); + } + + if (collection == null || session.getEncryptedSession() == null) return Optional.empty(); + + unlock(); + + DBusPath item = null; + final Map properties = Item.createProperties(label, attributes); + try (final Secret secret = session.getEncryptedSession().encrypt(password)) { + Pair response = collection.createItem(properties, secret, false).get(); + if (response == null) return null; + item = response.a; + if ("/".equals(item.getPath())) { + org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(response.b); + if (!completed.dismissed) { + org.freedesktop.secret.Collection.ItemCreated ic = collection + .getSignalHandler() + .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class); + item = ic.item; + } + } + } catch (NoSuchAlgorithmException | + NoSuchPaddingException | + InvalidAlgorithmParameterException | + InvalidKeyException | + BadPaddingException | + IllegalBlockSizeException e) { + log.error("Cloud not encrypt the secret.", e); + } + + if (null != item) { + return Optional.of(item.getPath()); + } else { + return Optional.empty(); + } + } + + @Override + public boolean delete() { + return false; + } + + @Override + public boolean deleteItem(String objectPath) { + return false; + } + + @Override + public boolean deleteItems(List objectPaths) { + return false; + } + + @Override + public Optional> getAttributes(String objectPath) { + return Optional.empty(); + } + + @Override + public Optional> getItems(Map attributes) { + return Optional.empty(); + } + + @Override + public Optional getLabel(String objectPath) { + return Optional.empty(); + } + + @Override + public Optional setLabel(String objectPath) { + return Optional.empty(); + } + + @Override + public Optional getSecret(String objectPath) { + if (Static.Utils.isNullOrEmpty(objectPath)) return Optional.empty(); + unlock(); + + final Item item = getItem(objectPath); + + char[] decrypted = null; + ObjectPath sessionPath = session.getSession().getPath(); + try (final Secret secret = item.getSecret(sessionPath).orElseGet(() -> new Secret(sessionPath, null))) { + decrypted = session.getEncryptedSession().decrypt(secret); + } catch (NoSuchPaddingException | + NoSuchAlgorithmException | + InvalidAlgorithmParameterException | + InvalidKeyException | + BadPaddingException | + IllegalBlockSizeException e) { + log.error("Could not decrypt the secret.", e); + return Optional.empty(); + } + if (decrypted == null) { + return Optional.empty(); + } else { + return Optional.of(decrypted); + } + } + + @Override + public Optional> getSecrets() { + return Optional.empty(); + } + + @Override + public boolean isLocked() { + return false; + } + + @Override + public boolean lock() { + return false; + } + + private void unlock() { + if (collection != null && collection.isLocked()) { + if (encrypted == null || isDefault()) { + Pair, ObjectPath> response = service.getService().unlock(lockable()).get(); + performPrompt(response.b); + if (!collection.isLocked()) { + isUnlockedOnceWithUserPermission = true; + log.info("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + } + } else if (service.isOrgGnomeKeyringAvailable()) { + InternalUnsupportedGuiltRiddenInterface withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service.getService()); + withoutPrompt.unlockWithMasterPassword(collection.getPath(), encrypted); + log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + } + } + } + + @Override + public boolean unlockWithUserPermission() { + return false; + } + + @Override + public boolean updateItem(String objectPath, String label, CharSequence password, Map attributes) { + return false; + } + + @Override + public void close() throws Exception { + + } + + private Map getLabels() { + List collections = service.getService().getCollections().get(); + + Map labels = new HashMap(); + for (ObjectPath path : collections) { + org.freedesktop.secret.Collection c = new org.freedesktop.secret.Collection(path, service.getService(), null); + labels.put(path, c.getLabel().get()); + } + + return labels; + } + + private boolean exists(String label) { + Map labels = getLabels(); + return labels.containsValue(label); + } + + private ObjectPath getCollectionPath(String label) { + Map labels = getLabels(); + + ObjectPath path = null; + for (Map.Entry entry : labels.entrySet()) { + ObjectPath p = entry.getKey(); + String l = entry.getValue(); + if (label.equals(l)) { + path = p; + break; + } + } + return path; + } + + private boolean isDefault() { + if (connection != null && connection.isConnected()) { + List defaults = Arrays.asList(null, "login", "session", "default"); + return defaults.contains(collection.getId()); + } else { + log.error("No D-Bus connection: Cannot check if the collection is the default collection."); + return false; + } + } + + private void performPrompt(ObjectPath path) { + if (!("/".equals(path.getPath()))) { + prompt.await(path, timeout); + } + } + + private Item getItem(String path) { + if (path != null) { + return new Item(Static.Convert.toObjectPath(path), service.getService()); + } else { + return null; + } + } + + private List lockable() { + return Arrays.asList(collection.getPath()); + } +} diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java new file mode 100644 index 0000000..70ba2d3 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -0,0 +1,91 @@ +package de.swiesend.secret.functional; + +import de.swiesend.secret.functional.interfaces.ServiceInterface; +import org.freedesktop.secret.TransportEncryption; +import de.swiesend.secret.functional.interfaces.CollectionInterface; +import de.swiesend.secret.functional.interfaces.SessionInterface; +import org.freedesktop.secret.Service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.UUID; + +public class Session implements SessionInterface { + + private static final Logger log = LoggerFactory.getLogger(Session.class); + + @Override + public TransportEncryption.EncryptedSession getEncryptedSession() { + return encryptedSession; + } + + public TransportEncryption.EncryptedSession encryptedSession = null; + + private UUID id = null; + + @Override + public org.freedesktop.secret.Session getSession() { + return session; + } + + org.freedesktop.secret.Session session = null; + + @Override + public ServiceInterface getService() { + return service; + } + + private ServiceInterface service = null; + + private Session(ServiceInterface service, TransportEncryption.EncryptedSession encryptedSession) { + this.id = UUID.randomUUID(); + this.service = service; + this.encryptedSession = encryptedSession; + this.session = service.getService().getSession(); + } + + public static Optional open(ServiceInterface service) { + + Service dbusService = service.getService(); + + return new TransportEncryption(dbusService) + .initialize() + .flatMap(initialized -> initialized.openSession()) + .flatMap(opened -> opened.generateSessionKey()) + .map(encryptedSession -> { + Session session = new Session(service, encryptedSession); + service.registerSession(session); + return session; + }) + .or(() -> { + log.error("Could not open transport encrypted session."); + return Optional.empty(); + }); + } + + @Override + public boolean clear() { + // TODO: to be implemented + return false; + } + + @Override + public Optional collection(String label, CharSequence password) { + return Optional.of(new Collection(this, label, password)); + } + + @Override + public Optional defaultCollection() { + return Optional.of(new Collection(this)); + } + + @Override + public void close() { + + } + + public UUID getId() { + return id; + } +} diff --git a/src/main/java/de/swiesend/secret/functional/System.java b/src/main/java/de/swiesend/secret/functional/System.java new file mode 100644 index 0000000..18b7284 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/System.java @@ -0,0 +1,66 @@ +package de.swiesend.secret.functional; + +import de.swiesend.secret.functional.interfaces.SystemInterface; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public class System extends SystemInterface { + + private static final Logger log = LoggerFactory.getLogger(System.class); + + private static DBusConnection connection = null; + + private System(DBusConnection connection) { + this.connection = connection; + } + + /** + * Try to get a new DBus connection. + * + * @return a new DBusConnection or Optional.empty() + */ + public static Optional connect() { + try { + DBusConnection dbus = DBusConnection.newConnection(DBusConnection.DBusBusType.SESSION); + System c = new System(dbus); + return Optional.of(c); + } catch (DBusException e) { + if (e == null) { + log.warn("Could not communicate properly with the D-Bus."); + } else { + log.warn(String.format("Could not communicate properly with the D-Bus: [%s]: %s", e.getClass().getSimpleName(), e.getMessage())); + } + } + return Optional.empty(); + } + + public static boolean isConnected() { + if (connection == null) { + return false; + } else { + return connection.isConnected(); + } + } + + @Override + public DBusConnection getConnection() { + return connection; + } + + synchronized public boolean disconnect() { + connection.disconnect(); + return connection.isConnected(); + } + + /*private static Optional setupShutdownHook() { + return Optional.empty(); + }*/ + @Override + public void close() throws Exception { + connection.close(); + } +} diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java new file mode 100644 index 0000000..7f95bb9 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java @@ -0,0 +1,26 @@ +package de.swiesend.secret.functional.interfaces; + +import de.swiesend.secret.functional.System; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +public abstract class SystemInterface implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(SystemInterface.class); + + public static Optional connect() { + log.warn("Do not call the interface method, but the implementation."); + return null; + } + + synchronized public boolean disconnect() { + log.warn("Do not call the interface method, but the implementation."); + return false; + } + + abstract public DBusConnection getConnection(); + +} diff --git a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java new file mode 100644 index 0000000..5fae466 --- /dev/null +++ b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java @@ -0,0 +1,34 @@ +package de.swiesend.secret.functional; + +import org.freedesktop.dbus.exceptions.DBusException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SecretServiceTest { + + private static final Logger log = LoggerFactory.getLogger(SecretServiceTest.class); + + @Test + @DisplayName("Fiddling around") + public void fiddlingAround() throws DBusException { + + char[] password = SecretService.create().flatMap( + service -> service.openSession().flatMap( + session -> session.collection("test", "test").flatMap( + collection -> collection.createItem("label", "password", Map.of("key", "value")).flatMap( + path -> collection.getSecret(path) + ) + ) + ) + ).get(); + + assertEquals("password", new String(password)); + } + +} From b690696be1c6d583a3265d3759bfba7484ece0a2 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 16 Jan 2023 19:23:48 +0100 Subject: [PATCH 17/74] Add autoclose --- .../secret/functional/Collection.java | 5 +- .../secret/functional/SecretService.java | 31 ++++++-- .../swiesend/secret/functional/Session.java | 67 ++++++++++------- .../de/swiesend/secretservice/Service.java | 9 ++- .../secretservice/TransportEncryption.java | 17 ++++- .../handlers/MessageHandler.java | 2 + .../simple/SimpleCollection.java | 2 +- .../secret/functional/SecretServiceTest.java | 70 ++++++++++++++--- .../integration/IntegrationTest.java | 2 +- .../secretservice/integration/ItemTest.java | 28 +++++-- ...alUnsupportedGuiltRiddenInterfaceTest.java | 12 ++- .../integration/test/Context.java | 75 ++++++++++++------- 12 files changed, 221 insertions(+), 99 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index f4706a7..1ab28bc 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -65,6 +65,7 @@ public Collection(SessionInterface session) { private void init(SessionInterface session) { this.session = session; + // TODO: do not always refer to the default collection ObjectPath path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); collection = new org.freedesktop.secret.Collection(path, session.getService().getService()); prompt = new Prompt(session.getService().getService()); @@ -233,7 +234,9 @@ public boolean updateItem(String objectPath, String label, CharSequence password @Override public void close() throws Exception { - + // TODO: remove log.info + log.info("collection close is triggered"); + if (this.encrypted != null) this.encrypted.close(); } private Map getLabels() { diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java index 6183729..3aac555 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -15,6 +15,7 @@ import java.time.Duration; import java.util.*; +import java.util.Collection; enum Activatable { @@ -81,11 +82,13 @@ public class SecretService extends ServiceInterface { // private Prompt prompt = null; // private InternalUnsupportedGuiltRiddenInterface withoutPrompt = null; // private org.freedesktop.secret.Session session = null; - private boolean isOrgGnomeKeyringAvailable = false; + + private boolean gnomeKeyringAvailable = false; + private SecretService(SystemInterface system, AvailableServices available) { this.service = new Service(system.getConnection()); - this.isOrgGnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); + this.gnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); } /** @@ -147,7 +150,7 @@ private static boolean isAvailable(System system, AvailableServices available) { @Override public boolean isOrgGnomeKeyringAvailable() { - return isOrgGnomeKeyringAvailable; + return this.gnomeKeyringAvailable; } @Override @@ -156,15 +159,21 @@ public boolean clear() { } @Override - public Optional openSession() { - return Session.open(this); + public Optional openSession() { + Optional session = Session + .open(this) + .map(s -> { + registerSession(s); + return s; + }); + return session; } - public void registerSession(Session session) { + private void registerSession(SessionInterface session) { this.sessions.put(session.getId(), session); } - public void unregisterSession(Session session) { + private void unregisterSession(SessionInterface session) { this.sessions.remove(session.getId()); } @@ -190,7 +199,13 @@ public void setTimeout(Duration timeout) { @Override public void close() throws Exception { - + // TODO: remove log.info + log.info("service close is triggered"); + List values = this.sessions.values().stream().toList(); + for (SessionInterface session : values) { + unregisterSession(session); + session.close(); + } } public Service getService() { diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java index 70ba2d3..7e2a646 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -1,51 +1,34 @@ package de.swiesend.secret.functional; -import de.swiesend.secret.functional.interfaces.ServiceInterface; -import org.freedesktop.secret.TransportEncryption; import de.swiesend.secret.functional.interfaces.CollectionInterface; +import de.swiesend.secret.functional.interfaces.ServiceInterface; import de.swiesend.secret.functional.interfaces.SessionInterface; import org.freedesktop.secret.Service; +import org.freedesktop.secret.TransportEncryption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.UUID; public class Session implements SessionInterface { private static final Logger log = LoggerFactory.getLogger(Session.class); - - @Override - public TransportEncryption.EncryptedSession getEncryptedSession() { - return encryptedSession; - } - public TransportEncryption.EncryptedSession encryptedSession = null; + private List collections = new ArrayList<>(); private UUID id = null; - - @Override - public org.freedesktop.secret.Session getSession() { - return session; - } - - org.freedesktop.secret.Session session = null; - - @Override - public ServiceInterface getService() { - return service; - } - private ServiceInterface service = null; private Session(ServiceInterface service, TransportEncryption.EncryptedSession encryptedSession) { this.id = UUID.randomUUID(); this.service = service; this.encryptedSession = encryptedSession; - this.session = service.getService().getSession(); } - public static Optional open(ServiceInterface service) { + public static Optional open(ServiceInterface service) { Service dbusService = service.getService(); @@ -55,8 +38,7 @@ public static Optional open(ServiceInterface service) { .flatMap(opened -> opened.generateSessionKey()) .map(encryptedSession -> { Session session = new Session(service, encryptedSession); - service.registerSession(session); - return session; + return (SessionInterface) session; }) .or(() -> { log.error("Could not open transport encrypted session."); @@ -64,6 +46,26 @@ public static Optional open(ServiceInterface service) { }); } + @Override + public TransportEncryption.EncryptedSession getEncryptedSession() { + return encryptedSession; + } + + @Override + public org.freedesktop.secret.Session getSession() { + return this.encryptedSession.getSession(); + } + + @Override + public List getCollections() { + return this.collections; + } + + @Override + public ServiceInterface getService() { + return service; + } + @Override public boolean clear() { // TODO: to be implemented @@ -72,19 +74,26 @@ public boolean clear() { @Override public Optional collection(String label, CharSequence password) { - return Optional.of(new Collection(this, label, password)); + CollectionInterface collection = new Collection(this, label, password); + this.collections.add(collection); + return Optional.of(collection); } @Override public Optional defaultCollection() { - return Optional.of(new Collection(this)); + CollectionInterface collection = new Collection(this); + this.collections.add(collection); + return Optional.of(collection); } @Override - public void close() { - + public void close() throws Exception { + for (CollectionInterface collection : this.collections) { + collection.close(); + } } + @Override public UUID getId() { return id; } diff --git a/src/main/java/de/swiesend/secretservice/Service.java b/src/main/java/de/swiesend/secretservice/Service.java index 288b6fd..341c7b1 100644 --- a/src/main/java/de/swiesend/secretservice/Service.java +++ b/src/main/java/de/swiesend/secretservice/Service.java @@ -13,7 +13,7 @@ public class Service extends Messaging implements de.swiesend.secretservice.inte public static final List> signals = Arrays.asList( CollectionCreated.class, CollectionChanged.class, CollectionDeleted.class); private static final Logger log = LoggerFactory.getLogger(Service.class); - private Session session = null; + // private Session session = null; public Service(DBusConnection connection) { super(connection, signals, @@ -29,7 +29,8 @@ public Optional, ObjectPath>> openSession(String algorithm, .flatMap(response -> Optional.of(new Pair<>((Variant) response[0], (ObjectPath) response[1]))) .map(pair -> { log.debug("Got session: " + pair.b.getPath()); - session = new Session(pair.b, this); + // TODO: remove session creation from service + // session = new Session(pair.b, this); return pair; }); } @@ -117,8 +118,8 @@ public String getObjectPath() { return Static.ObjectPaths.SECRETS; } - public Session getSession() { + /*public Session getSession() { return session; - } + }*/ } diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index 240e969..b6d3a98 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -36,6 +36,8 @@ public class TransportEncryption implements AutoCloseable { private SecretKey sessionKey = null; private byte[] yb = null; + private Session session = null; + public TransportEncryption() throws DBusException { DBusConnection connection = DBusConnectionBuilder.forSessionBus().withShared(false).build(); this.service = new Service(connection); @@ -83,6 +85,10 @@ public Service getService() { return service; } + public Session getSession() { + return session; + } + public void clear() { if (privateKey != null) try { privateKey.destroy(); @@ -99,6 +105,7 @@ public void clear() { @Override public void close() { clear(); + if (session != null) session.close(); } public class InitializedSession { @@ -118,6 +125,9 @@ public Optional openSession() { ).flatMap(pair -> { // transform peer's raw Y to a public key yb = pair.a.getValue(); + + ObjectPath sessionPath = pair.b; + session = new Session(sessionPath, service); return Optional.of(new OpenedSession()); }); } @@ -159,6 +169,11 @@ public Optional generateSessionKey() { } public class EncryptedSession { + + public Session getSession() { + return session; + } + public Secret encrypt(CharSequence plain) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { @@ -193,7 +208,7 @@ public Secret encrypt(byte[] plain, Charset charset) throws NoSuchAlgorithmExcep String contentType = Secret.createContentType(charset); - return new Secret(service.getSession().getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType); + return new Secret(session.getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType); } public char[] decrypt(Secret secret) throws NoSuchPaddingException, diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 322d915..598b7a9 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -72,6 +72,8 @@ public Optional send(String service, String path, String iface, String case "org.freedesktop.DBus.Error.ServiceUnknown": case "org.freedesktop.DBus.Error.UnknownMethod": case "org.freedesktop.DBus.Error.UnknownObject": + case "org.freedesktop.DBus.Error.InvalidArgs": + log.error(error + ": " + Arrays.deepToString(parameters)); case "org.freedesktop.DBus.Local.Disconnected": case "org.freedesktop.dbus.exceptions.FatalDBusException": case "org.freedesktop.dbus.exceptions.NotConnected": diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 91364b9..079562c 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -287,7 +287,7 @@ private void init() throws IOException { () -> new IOException("Could not initiate transport encryption.") ); service = transport.getService(); - session = service.getSession(); + session = transport.getSession(); prompt = new Prompt(service); withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); } diff --git a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java index 5fae466..7827170 100644 --- a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java +++ b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java @@ -1,6 +1,8 @@ package de.swiesend.secret.functional; -import org.freedesktop.dbus.exceptions.DBusException; +import de.swiesend.secret.functional.interfaces.CollectionInterface; +import de.swiesend.secret.functional.interfaces.ServiceInterface; +import de.swiesend.secret.functional.interfaces.SessionInterface; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; @@ -16,19 +18,63 @@ public class SecretServiceTest { @Test @DisplayName("Fiddling around") - public void fiddlingAround() throws DBusException { - - char[] password = SecretService.create().flatMap( - service -> service.openSession().flatMap( - session -> session.collection("test", "test").flatMap( - collection -> collection.createItem("label", "password", Map.of("key", "value")).flatMap( - path -> collection.getSecret(path) - ) - ) - ) - ).get(); + public void fiddlingAround() throws Exception { + char[] password; + try (ServiceInterface service = SecretService.create().get()) { + CollectionInterface collection = service.openSession().flatMap(session -> + session.collection("test", "test")).get(); + Map attributes = Map.of("key", "value"); + String item = collection.createItem("label", "password", attributes).get(); + password = collection.getSecret(item).get(); + /*for (String i : collection.getItems(attributes).get()) { + log.info("[" + collection.getLabel(item).get() + "]" + i); + // collection.deleteItem(i); + }*/ + // collection.delete(); + + CollectionInterface col2 = service.openSession().flatMap(session -> + session.collection("test", "test")).get(); + } + assertEquals("password", new String(password)); + password = "".toCharArray(); + + /*try (ServiceInterface service = SecretService.create().get()) { + try (SessionInterface session = service.openSession().get()) { + try (CollectionInterface collection = session.collection("test", "test").get()) { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + password = secret.get(); + } + } + } assertEquals("password", new String(password)); + password = "".toCharArray(); + + password = SecretService.create().flatMap( + service -> service.openSession()).flatMap( + session -> session.collection("test", "test")).flatMap( + collection -> { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + return secret; + }).get(); + + assertEquals("password", new String(password));*/ + + + /*try (ServiceInterface service = SecretService.create().get()){ + try (SessionInterface session = service.openSession().get()) { + try (CollectionInterface collection = session.collection("test", "test").get()) { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + secret; + } + } + }*/ } } diff --git a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java index 2457332..c5554db 100644 --- a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java @@ -62,7 +62,7 @@ public void testWithTransportEncryption() throws assertEquals(plain, new String(decrypted)); Service service = transportEncryption.getService(); - Session session = service.getSession(); + Session session = transportEncryption.getSession(); InternalUnsupportedGuiltRiddenInterface noPrompt = new InternalUnsupportedGuiltRiddenInterface(service); Secret master = encryptedSession.encrypt("test"); diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index 7451c54..dfb5c33 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -9,14 +9,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; import static de.swiesend.secretservice.integration.test.Context.label; import static org.junit.jupiter.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.assertEquals; public class ItemTest { @@ -58,9 +63,19 @@ public void getSecret() { String parameters = new String(secret.getSecretParameters(), StandardCharsets.UTF_8); log.info(label("parameters", parameters)); - assertEquals("", parameters); - - String value = new String(secret.getSecretValue(), StandardCharsets.UTF_8); + if (context.encrypted == false) assertEquals("", parameters); + + String value; + if (context.encrypted == false) { + value = new String(secret.getSecretValue(), StandardCharsets.UTF_8); + } else { + try { + value = new String(context.encryption.decrypt(secret)); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | + InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + throw new RuntimeException(e); + } + } log.info(label("value", value)); assertEquals("super secret", value); } @@ -79,8 +94,9 @@ public void getForeignSecret() { Collection login = new Collection(alias, context.service); List items = login.getItems().get(); Item item = new Item(items.get(0), context.service); - Secret secret = item.getSecret(context.service.getSession().getPath()).get(); - log.info(new String(secret.getSecretValue())); + Secret secret = item.getSecret(context.session.getPath()).get(); + log.info("'" + new String(secret.getSecretValue()) + "' [" + new String(secret.getSecretParameters()) + "]"); + } @Test diff --git a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java index 4804d0f..6226044 100644 --- a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java @@ -1,15 +1,12 @@ package de.swiesend.secretservice.integration.keyring; +import de.swiesend.secretservice.*; import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.types.Variant; -import de.swiesend.secretservice.Collection; -import de.swiesend.secretservice.Secret; -import de.swiesend.secretservice.Service; -import de.swiesend.secretservice.Static; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -35,10 +32,11 @@ public class InternalUnsupportedGuiltRiddenInterfaceTest { public void beforeEach() throws DBusException { connection = DBusConnectionBuilder.forSessionBus().withShared(false).build(); service = new Service(connection); - service.openSession(Static.Algorithm.PLAIN, new Variant("")); + Pair, ObjectPath> pair = service.openSession(Static.Algorithm.PLAIN, new Variant("")).get(); + ObjectPath sessionPath = pair.b; iugri = new InternalUnsupportedGuiltRiddenInterface(service); - original = new Secret(service.getSession().getPath(), "".getBytes(), "test".getBytes()); - master = new Secret(service.getSession().getPath(), "".getBytes(), "master-secret".getBytes()); + original = new Secret(sessionPath, "".getBytes(), "test".getBytes()); + master = new Secret(sessionPath, "".getBytes(), "master-secret".getBytes()); collection = new Collection("test", service); } diff --git a/src/test/java/de/swiesend/secretservice/integration/test/Context.java b/src/test/java/de/swiesend/secretservice/integration/test/Context.java index 8d845a2..4c45658 100644 --- a/src/test/java/de/swiesend/secretservice/integration/test/Context.java +++ b/src/test/java/de/swiesend/secretservice/integration/test/Context.java @@ -9,8 +9,14 @@ import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.slf4j.Logger; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import java.io.IOException; import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -37,7 +43,7 @@ public class Context { public Context(Logger log) { this.log = log; - this.encrypted = false; + this.encrypted = true; } public Context(Logger log, boolean encrypted) { @@ -45,6 +51,30 @@ public Context(Logger log, boolean encrypted) { this.encrypted = encrypted; } + public static String label(String name, String msg) { + return name + ": \"" + msg + "\""; + } + + public static String label(String name, int number) { + return name + ": " + number; + } + + public static String label(String name, long number) { + return name + ": " + number; + } + + public static String label(String name, BigInteger number) { + return name + ": " + number; + } + + public static String label(String name, byte[] bytes) { + return name + ": " + Arrays.toString(bytes); + } + + public static String label(String name, List list) { + return name + ": " + Arrays.toString(list.toArray()); + } + public void ensureService() { DBusConnection connection = null; try { @@ -58,7 +88,7 @@ public void ensureService() { prompt = new Prompt(service); } - public void ensureSession() { + public void ensureSession() throws RuntimeException { ensureService(); try { @@ -70,20 +100,31 @@ public void ensureSession() { .orElseThrow( () -> new IOException("Could not initiate transport encryption.") ); + session = encryption.getSession(); } else { - service.openSession(Static.Algorithm.PLAIN, new Variant("")); + Pair, ObjectPath> pair = service.openSession(Static.Algorithm.PLAIN, new Variant("")).get(); + session = new Session(pair.b, service); } } catch (Exception e) { log.error("Could not establish transport encryption.", e); exit(-2); } - session = service.getSession(); log.info(session.getObjectPath()); assertNotNull(session); assertTrue(session.getObjectPath().startsWith(Static.ObjectPaths.SESSION + "/s")); - password = new Secret(session.getPath(), "".getBytes(), "test".getBytes()); + String test = "test"; + if (encrypted) { + try { + password = encryption.encrypt(test); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | + InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { + throw new RuntimeException(e); + } + } else { + password = new Secret(session.getPath(), "".getBytes(), test.getBytes()); + } } public void ensureCollection() { @@ -157,29 +198,5 @@ public void after() { } } - public static String label(String name, String msg) { - return name + ": \"" + msg + "\""; - } - - public static String label(String name, int number) { - return name + ": " + number; - } - - public static String label(String name, long number) { - return name + ": " + number; - } - - public static String label(String name, BigInteger number) { - return name + ": " + number; - } - - public static String label(String name, byte[] bytes) { - return name + ": " + Arrays.toString(bytes); - } - - public static String label(String name, List list) { - return name + ": " + Arrays.toString(list.toArray()); - } - } From 40a3f1cf8bcff57a59b57a5703424040787e3fbd Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:08:04 +0200 Subject: [PATCH 18/74] Change Messaging Interface to returning `Optional` --- .../secret/functional/Collection.java | 2 +- .../de/swiesend/secretservice/Collection.java | 13 +-- .../de/swiesend/secretservice/Service.java | 7 -- .../de/swiesend/secretservice/Static.java | 4 + .../secretservice/TransportEncryption.java | 74 +++++++++------ .../simple/SimpleCollection.java | 92 +++++++++---------- .../integration/CollectionTest.java | 20 ++-- .../integration/IntegrationTest.java | 12 +-- .../secretservice/integration/ItemTest.java | 2 +- .../secretservice/integration/PromptTest.java | 2 +- .../integration/ServiceTest.java | 2 +- ...alUnsupportedGuiltRiddenInterfaceTest.java | 2 +- .../integration/test/Context.java | 15 +-- 13 files changed, 124 insertions(+), 123 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index 1ab28bc..e404009 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -118,7 +118,7 @@ public Optional createItem(String label, CharSequence password, Map> signals) { - super(service.getConnection(), signals, + public Collection(DBusPath path, DBusConnection connection, List> signals) { + super(connection, signals, Static.Service.SECRETS, path.getPath(), Static.Interfaces.COLLECTION); @@ -34,8 +35,8 @@ public Collection(DBusPath path, Service service, List> signals = Arrays.asList( CollectionCreated.class, CollectionChanged.class, CollectionDeleted.class); private static final Logger log = LoggerFactory.getLogger(Service.class); - // private Session session = null; public Service(DBusConnection connection) { super(connection, signals, @@ -29,8 +28,6 @@ public Optional, ObjectPath>> openSession(String algorithm, .flatMap(response -> Optional.of(new Pair<>((Variant) response[0], (ObjectPath) response[1]))) .map(pair -> { log.debug("Got session: " + pair.b.getPath()); - // TODO: remove session creation from service - // session = new Session(pair.b, this); return pair; }); } @@ -118,8 +115,4 @@ public String getObjectPath() { return Static.ObjectPaths.SECRETS; } - /*public Session getSession() { - return session; - }*/ - } diff --git a/src/main/java/de/swiesend/secretservice/Static.java b/src/main/java/de/swiesend/secretservice/Static.java index 8fdd906..4632aa4 100644 --- a/src/main/java/de/swiesend/secretservice/Static.java +++ b/src/main/java/de/swiesend/secretservice/Static.java @@ -205,6 +205,10 @@ public static boolean isNullOrEmpty(final CharSequence cs) { return cs == null || cs.toString().trim().isEmpty(); } + public static boolean isNullOrEmpty(final byte[] bytes) { + return bytes == null || bytes.toString().trim().isEmpty(); + } + public static boolean isNullOrEmpty(final String s) { return s == null || s.trim().isEmpty(); } diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index b6d3a98..e0559f3 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -174,44 +174,58 @@ public Session getSession() { return session; } - public Secret encrypt(CharSequence plain) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - + // TODO: fix interface usage + public Optional encrypt(CharSequence plain) { final byte[] bytes = Secret.toBytes(plain); - try { - return encrypt(bytes, StandardCharsets.UTF_8); - } finally { - Secret.clear(bytes); - } + Optional secret = encrypt(bytes, StandardCharsets.UTF_8); + Secret.clear(bytes); + plain = null; // TODO: find a better way to clear the CharSequence + return secret; } - public Secret encrypt(byte[] plain, Charset charset) throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { - - if (plain == null) return null; - - if (service == null) { - throw new IllegalStateException("Missing session. Call Initialized.openSession() first."); - } - if (sessionKey == null) { - throw new IllegalStateException("Missing session key. Call Opened.generateSessionKey() first."); - } - - // secret.parameter - 16 byte AES initialization vector - final byte[] salt = new byte[toBytes(AES_BITS)]; - SecureRandom random = SecureRandom.getInstance(Static.Algorithm.SHA1_PRNG); - random.nextBytes(salt); - IvParameterSpec ivSpec = new IvParameterSpec(salt); + // TODO: fix interface usage + public Optional encrypt(byte[] plain, Charset charset) { - Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); - cipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivSpec); + if (Static.Utils.isNullOrEmpty(plain)) return Optional.empty(); - String contentType = Secret.createContentType(charset); + try { + if (service == null) { + log.error("Missing session. Call Initialized.openSession() first."); + return Optional.empty(); + } + if (sessionKey == null) { + log.error("Missing session key. Call Opened.generateSessionKey() first."); + return Optional.empty(); + } - return new Secret(session.getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType); + // secret.parameter - 16 byte AES initialization vector + final byte[] salt = new byte[toBytes(AES_BITS)]; + SecureRandom random = SecureRandom.getInstance(Static.Algorithm.SHA1_PRNG); + random.nextBytes(salt); + IvParameterSpec ivSpec = new IvParameterSpec(salt); + + Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); + cipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivSpec); + + String contentType = Secret.createContentType(charset); + + return Optional.of(new Secret(session.getPath(), ivSpec.getIV(), cipher.doFinal(plain), contentType)); + } catch (InvalidAlgorithmParameterException | + InvalidKeyException | + NoSuchPaddingException | + BadPaddingException | + IllegalBlockSizeException | + NoSuchAlgorithmException e) { + log.error("Could not encrypt the secret", e); + } finally { + Secret.clear(plain); + } + return Optional.empty(); } - public char[] decrypt(Secret secret) throws NoSuchPaddingException, + // TODO: return Optional; remove exceptions; + public char[] decrypt(Secret secret) throws + NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 079562c..56a2c1a 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -56,7 +56,7 @@ public SimpleCollection() throws IOException { try { init(); ObjectPath path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); - collection = new Collection(path, service); + collection = new Collection(path, connection); } catch (RuntimeException e) { throw new IOException("Could not initialize the secret service.", e); } @@ -82,20 +82,16 @@ public SimpleCollection(String label, CharSequence password) throws IOException try { init(); if (password != null) { - try { - encrypted = transportEncryptedSession.encrypt(password); - } catch (NoSuchAlgorithmException | - NoSuchPaddingException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not establish transport encryption.", e); + Optional maybeEncrypted = transportEncryptedSession.encrypt(password); + if (maybeEncrypted.isPresent()) { + encrypted = maybeEncrypted.get(); + } else { + log.error("Could not establish transport encryption."); } } if (exists(label)) { ObjectPath path = getCollectionPath(label); - collection = new Collection(path, service); + collection = new Collection(path, connection); } else { DBusPath path = null; Map properties = Collection.createProperties(label); @@ -122,7 +118,7 @@ public SimpleCollection(String label, CharSequence password) throws IOException if (path == null) throw new IOException("Could not acquire a path for the prompt."); - collection = new Collection(path, service); + collection = new Collection(path, connection); } } catch (RuntimeException e) { throw new IOException("Could not initialize the secret service.", e); @@ -297,7 +293,7 @@ private Map getLabels() { Map labels = new HashMap(); for (ObjectPath path : collections) { - Collection c = new Collection(path, service, null); + Collection c = new Collection(path, connection, null); labels.put(path, c.getLabel().get()); } @@ -467,33 +463,32 @@ public String createItem(String label, CharSequence password, Map properties = Item.createProperties(label, attributes); - try (final Secret secret = transportEncryptedSession.encrypt(password)) { - Pair response = collection.createItem(properties, secret, false).get(); - if (response == null) return null; - item = response.a; - if ("/".equals(item.getPath())) { - Completed completed = prompt.await(response.b); - if (!completed.dismissed) { - Collection.ItemCreated ic = collection.getSignalHandler().getLastHandledSignal(Collection.ItemCreated.class); - item = ic.item; - } - } - } catch (NoSuchAlgorithmException | - NoSuchPaddingException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not encrypt the secret.", e); - } - - if (null != item) { - return item.getPath(); - } else { - return null; - } + return transportEncryptedSession + .encrypt(password) + .flatMap(secret -> { + try (secret) { // auto-close + final Map properties = Item.createProperties(label, attributes); + return collection.createItem(properties, secret, false) + .flatMap(pair -> Optional.ofNullable(pair.a) + .map(item -> { + if ("/".equals(item.getPath())) { // prompt required + org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(pair.b); + if (completed.dismissed) { + return item; + } else { + return collection + .getSignalHandler() + .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class) + .item; + } + } else { + return item; + } + }) + .map(DBusPath::getPath)); + } + }) + .orElse(null); } /** @@ -537,15 +532,14 @@ public void updateItem(String objectPath, String label, CharSequence password, M item.setAttributes(attributes); } - if (password != null) try (Secret secret = transportEncryptedSession.encrypt(password)) { - item.setSecret(secret); - } catch (NoSuchAlgorithmException | - NoSuchPaddingException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not encrypt the secret.", e); + if (password != null) { + transportEncryptedSession.encrypt(password).map(secret -> { + try (secret) { + boolean success = item.setSecret(secret); + if (!success) log.error("Could not set the secret."); + return success; + } + }); } } diff --git a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java index 7557fec..1375972 100644 --- a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java @@ -99,7 +99,7 @@ public void getItems() { @Test @Disabled public void getLabel() { - Collection collection = new Collection("login", context.service); + Collection collection = new Collection("login", context.service.getConnection()); String response = collection.getLabel().get(); log.info(response); List labels = Arrays.asList(new String[]{ @@ -139,17 +139,17 @@ public void created() { Collection collection; UInt64 response; - collection = new Collection("test", context.service); + collection = new Collection("test", context.service.getConnection()); response = collection.created().get(); log.info("test: " + response); assertTrue(response.longValue() >= 0L); - collection = new Collection("login", context.service); + collection = new Collection("login", context.service.getConnection()); response = collection.created().get(); log.info("login: " + response); assertTrue(response.longValue() >= 0L); - collection = new Collection("session", context.service); + collection = new Collection("session", context.service.getConnection()); response = collection.created().get(); log.info("session: " + response); assertTrue(response.longValue() == 0L); @@ -161,16 +161,16 @@ public void modified() { Collection collection; UInt64 response; - collection = new Collection("test", context.service); + collection = new Collection("test", context.service.getConnection()); response = collection.modified().get(); log.info("test: " + response); assertTrue(response.longValue() >= 0L); - collection = new Collection("login", context.service); + collection = new Collection("login", context.service.getConnection()); response = collection.modified().get(); log.info("login: " + response); assertTrue(response.longValue() >= 0L); - collection = new Collection("session", context.service); + collection = new Collection("session", context.service.getConnection()); response = collection.modified().get(); log.info("session: " + response); assertTrue(response.longValue() == 0L); @@ -178,7 +178,7 @@ public void modified() { @Test public void isRemote() { - Collection collection = new Collection("test", context.service); + Collection collection = new Collection("test", context.service.getConnection()); assertFalse(collection.isRemote()); } @@ -188,11 +188,11 @@ public void getObjectPath() { log.info(test); assertEquals("/org/freedesktop/secrets/collection/test", test); - Collection login = new Collection("login", context.service); + Collection login = new Collection("login", context.service.getConnection()); log.info(login.getObjectPath()); assertEquals("/org/freedesktop/secrets/collection/login", login.getObjectPath()); - Collection session = new Collection("session", context.service); + Collection session = new Collection("session", context.service.getConnection()); log.info(session.getObjectPath()); assertEquals("/org/freedesktop/secrets/collection/session", session.getObjectPath()); } diff --git a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java index c5554db..9e0f938 100644 --- a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java @@ -52,7 +52,7 @@ public void testWithTransportEncryption() throws ); String plain = "super secret"; - Secret encrypted = encryptedSession.encrypt(plain); + Secret encrypted = encryptedSession.encrypt(plain).get(); byte[] encBase64 = Base64.getEncoder().encode(encrypted.getSecretValue()); log.info(label("encrypted secret (base64)", new String(encBase64))); @@ -65,21 +65,21 @@ public void testWithTransportEncryption() throws Session session = transportEncryption.getSession(); InternalUnsupportedGuiltRiddenInterface noPrompt = new InternalUnsupportedGuiltRiddenInterface(service); - Secret master = encryptedSession.encrypt("test"); - Collection collection = new Collection("test", service); + Secret master = encryptedSession.encrypt("test").get(); + Collection collection = new Collection("test", service.getConnection()); List collections = Static.Convert.toStrings(service.getCollections().get()); if (collections.contains(collection.getObjectPath())) { noPrompt.unlockWithMasterPassword(collection.getPath(), master); } else { - HashMap properties = new HashMap(); - properties.put("org.freedesktop.Secret.Collection.Label", new Variant("test")); + HashMap properties = new HashMap<>(); + properties.put("org.freedesktop.Secret.Collection.Label", new Variant<>("test")); ObjectPath collectionPath = noPrompt.createWithMasterPassword(properties, master).get(); log.info("created collection: " + collectionPath.getPath()); } // create item with secret - Map attributes = new HashMap(); + Map attributes = new HashMap<>(); attributes.put("transport", "encrypted"); attributes.put("algorithm", "AES"); attributes.put("bits", "128"); diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index dfb5c33..6548537 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -91,7 +91,7 @@ public void getForeignSecret() { // DBusPath alias = new DBusPath(Static.ObjectPaths.DEFAULT_COLLECTION); - Collection login = new Collection(alias, context.service); + Collection login = new Collection(alias, context.service.getConnection()); List items = login.getItems().get(); Item item = new Item(items.get(0), context.service); Secret secret = item.getSecret(context.session.getPath()).get(); diff --git a/src/test/java/de/swiesend/secretservice/integration/PromptTest.java b/src/test/java/de/swiesend/secretservice/integration/PromptTest.java index 4596550..df4bb53 100644 --- a/src/test/java/de/swiesend/secretservice/integration/PromptTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/PromptTest.java @@ -64,7 +64,7 @@ public void dismissPrompt() throws InterruptedException { List cs = Arrays.asList(context.collection.getPath()); context.service.lock(cs); SignalHandler handler = context.service.getSignalHandler(); - Collection defaultCollection = new Collection("login", context.service); + Collection defaultCollection = new Collection("login", context.service.getConnection()); boolean expected = defaultCollection.isLocked(); Thread.currentThread().sleep(500L); diff --git a/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java b/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java index 1ed0a36..a963e51 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ServiceTest.java @@ -206,7 +206,7 @@ public void lockCommonCollections() throws InterruptedException, NoSuchObject { context.ensureSession(); // lock common collections: - // * alias/default == collection/login + // * alias/default == collection/login (if not assigned otherwise) // * collection/login // * collection/session ArrayList collections = new ArrayList(); diff --git a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java index 6226044..add7ab1 100644 --- a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java @@ -37,7 +37,7 @@ public void beforeEach() throws DBusException { iugri = new InternalUnsupportedGuiltRiddenInterface(service); original = new Secret(sessionPath, "".getBytes(), "test".getBytes()); master = new Secret(sessionPath, "".getBytes(), "master-secret".getBytes()); - collection = new Collection("test", service); + collection = new Collection("test", service.getConnection()); } @AfterEach diff --git a/src/test/java/de/swiesend/secretservice/integration/test/Context.java b/src/test/java/de/swiesend/secretservice/integration/test/Context.java index 4c45658..3aac9ff 100644 --- a/src/test/java/de/swiesend/secretservice/integration/test/Context.java +++ b/src/test/java/de/swiesend/secretservice/integration/test/Context.java @@ -102,7 +102,7 @@ public void ensureSession() throws RuntimeException { ); session = encryption.getSession(); } else { - Pair, ObjectPath> pair = service.openSession(Static.Algorithm.PLAIN, new Variant("")).get(); + Pair, ObjectPath> pair = service.openSession(Static.Algorithm.PLAIN, new Variant<>("")).get(); session = new Session(pair.b, service); } } catch (Exception e) { @@ -116,12 +116,7 @@ public void ensureSession() throws RuntimeException { String test = "test"; if (encrypted) { - try { - password = encryption.encrypt(test); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException | - InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { - throw new RuntimeException(e); - } + password = encryption.encrypt(test).get(); } else { password = new Secret(session.getPath(), "".getBytes(), test.getBytes()); } @@ -130,7 +125,7 @@ public void ensureSession() throws RuntimeException { public void ensureCollection() { ensureSession(); - collection = new Collection("test", service); + collection = new Collection("test", service.getConnection()); collections = Static.Convert.toStrings(service.getCollections().get()); if (collections.contains(Static.ObjectPaths.collection("test"))) { @@ -152,7 +147,7 @@ public void ensureCollection() { public void ensureItem() { ensureCollection(); - Map attributes = new HashMap(); + Map attributes = new HashMap<>(); attributes.put("Attribute1", "Value1"); attributes.put("Attribute2", "Value2"); @@ -161,7 +156,7 @@ public void ensureItem() { if (encrypted) { try { - secret = encryption.encrypt(plain); + secret = encryption.encrypt(plain).get(); attributes.put("TransportEncryption", "yes"); } catch (Exception e) { log.error("Could not encrypt the secret.", e); From c7f3b1e10f4309b8575edfcadd01e08ce81291cf Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:08:25 +0200 Subject: [PATCH 19/74] Add functional interfaces --- .../secret/functional/interfaces/SystemInterface.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java index 7f95bb9..829a7b8 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java +++ b/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java @@ -16,6 +16,11 @@ public static Optional connect() { return null; } + public static boolean isConnected() { + log.warn("Do not call the interface method, but the implementation."); + return false; + } + synchronized public boolean disconnect() { log.warn("Do not call the interface method, but the implementation."); return false; From be1ddb61ab3b3d8d199ea1d4d3b14edab4682e5a Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:08:38 +0200 Subject: [PATCH 20/74] Add functional implementations --- .../secret/functional/Collection.java | 476 +++++++++++++----- .../secret/functional/SecretService.java | 144 +----- .../swiesend/secret/functional/Session.java | 4 +- .../de/swiesend/secret/functional/System.java | 19 +- 4 files changed, 360 insertions(+), 283 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index e404009..a3dcd8c 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -21,146 +21,313 @@ import java.time.Duration; import java.util.*; -import static org.freedesktop.secret.Static.DEFAULT_PROMPT_TIMEOUT; +import static org.freedesktop.secret.Static.DBus.DEFAULT_DELAY_MILLIS; public class Collection implements CollectionInterface { private static final Logger log = LoggerFactory.getLogger(Collection.class); - /*this.prompt = new Prompt(service); - this.withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service); - */ + org.freedesktop.secret.Collection collection = null; SessionInterface session = null; ServiceInterface service = null; DBusConnection connection = null; - private Duration timeout = DEFAULT_PROMPT_TIMEOUT; + private Duration timeout = null; private Boolean isUnlockedOnceWithUserPermission = false; private String label = null; - private Secret encrypted = null; + private String id = null; + private Optional encryptedCollectionPassword = null; private Prompt prompt = null; + private InternalUnsupportedGuiltRiddenInterface withoutPrompt = null; + + private ObjectPath path = null; - public Collection(SessionInterface session, String label, CharSequence password) { + public Collection(SessionInterface session) { init(session); - this.label = label; - try { - this.encrypted = session.getEncryptedSession().encrypt(password); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (NoSuchPaddingException e) { - throw new RuntimeException(e); - } catch (InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } catch (InvalidKeyException e) { - throw new RuntimeException(e); - } catch (BadPaddingException e) { - throw new RuntimeException(e); - } catch (IllegalBlockSizeException e) { - throw new RuntimeException(e); - } + + this.path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); + this.collection = new org.freedesktop.secret.Collection(path, connection); + this.label = collection.getLabel().get(); + this.id = collection.getId(); } - public Collection(SessionInterface session) { + public Collection(SessionInterface session, String label, Optional maybePassword) { init(session); + + this.encryptedCollectionPassword = maybePassword + .flatMap(password -> session.getEncryptedSession().encrypt(password)); + + this.collection = getOrCreateCollection(label) + .orElseThrow(() -> new NoSuchElementException(String.format("Cloud not acquire collection with name %s", label))); + this.path = collection.getPath(); + this.label = label; + this.id = collection.getId(); } private void init(SessionInterface session) { this.session = session; - // TODO: do not always refer to the default collection - ObjectPath path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); - collection = new org.freedesktop.secret.Collection(path, session.getService().getService()); - prompt = new Prompt(session.getService().getService()); - log.info(collection.getId()); this.service = session.getService(); this.connection = service.getService().getConnection(); + this.timeout = session.getService().getTimeout(); + this.prompt = new Prompt(session.getService().getService()); + if (service.isOrgGnomeKeyringAvailable()) { + this.withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(session.getService().getService()); + } + } + + private Optional getOrCreateCollection(String label) { + Optional maybePath; + + if (exists(label)) { + maybePath = getCollectionPath(label); + } else { + maybePath = createNewCollection(label); + } + + return maybePath.flatMap(path -> getCollectionFromPath(path, label)); + } + + private Optional createNewCollection(String label) { + ObjectPath path = null; + Map properties = org.freedesktop.secret.Collection.createProperties(label); + + if (encryptedCollectionPassword.isEmpty()) { + path = createCollectionWithPrompt(properties); + } else if (service.isOrgGnomeKeyringAvailable()) { + path = withoutPrompt.createWithMasterPassword(properties, encryptedCollectionPassword.get()).get(); + } + + if (path == null) { + waitForCollectionCreatedSignal(); + Service.CollectionCreated signal = service.getService().getSignalHandler().getLastHandledSignal(Service.CollectionCreated.class); + DBusPath signalPath = signal.collection; + if (signalPath == null || signalPath.getPath() == null) { + log.error(String.format("Received bad signal `CollectionCreated` without proper collection path: %s", signal)); + return null; + } + path = Static.Convert.toObjectPath(signalPath.getPath()); + } + + if (path == null) { + log.error("Could not acquire a path for the prompt."); + return null; + } + + return Optional.ofNullable(path); + } + + private void waitForCollectionCreatedSignal() { + try { + Thread.currentThread().sleep(DEFAULT_DELAY_MILLIS); + } catch (InterruptedException e) { + log.error("Unexpected interrupt while waiting for a CollectionCreated signal.", e); + } + } + + private ObjectPath createCollectionWithPrompt(Map properties) { + Pair response = service.getService().createCollection(properties).get(); + if (!"/".equals(response.a.getPath())) { + return response.a; + } + performPrompt(response.b); + return null; + } + + private Optional getCollectionFromPath(ObjectPath path, String label) { + if (path == null) { + log.error(String.format("Could not acquire collection with label: \"%s\"", label)); + return Optional.empty(); + } + + collection = new org.freedesktop.secret.Collection(path, connection); + return Optional.of(collection); + } + + private Optional getCollectionPath(String label) { + Map labels = getLabels(); + + ObjectPath path = null; + for (Map.Entry entry : labels.entrySet()) { + ObjectPath p = entry.getKey(); + String l = entry.getValue(); + if (label.equals(l)) { + path = p; + break; + } + } + return Optional.ofNullable(path); + } + + private boolean isDefault() { + if (connection != null && connection.isConnected()) { + List defaults = Arrays.asList(null, "login", "session", "default"); + return defaults.contains(collection.getId()); + } else { + log.error("No D-Bus connection: Cannot check if the collection is the default collection."); + return false; + } + } + + private void performPrompt(ObjectPath path) { + if (!("/".equals(path.getPath()))) { + prompt.await(path, timeout); + } } + @Override public boolean clear() { - return false; + if (encryptedCollectionPassword.isPresent()) { + encryptedCollectionPassword.get().clear(); + // TODO: remove log statement + log.info("collection password: " + encryptedCollectionPassword.get().getSecretValue()); + } + return true; } @Override public Optional createItem(String label, CharSequence password) { - return Optional.empty(); + return createItem(label, password, null); } @Override public Optional createItem(String label, CharSequence password, Map attributes) { if (Static.Utils.isNullOrEmpty(password)) { - throw new IllegalArgumentException("The password may not be null or empty."); + log.error("The password may not be null or empty."); + return Optional.empty(); } if (label == null) { - throw new IllegalArgumentException("The label of the item may not be null."); + log.error("The label of the item may not be null."); + return Optional.empty(); } - if (collection == null || session.getEncryptedSession() == null) return Optional.empty(); + if (collection == null || session.getEncryptedSession() == null) { + log.error("Not collection or session"); + return Optional.empty(); + } unlock(); - DBusPath item = null; - final Map properties = Item.createProperties(label, attributes); - try (final Secret secret = session.getEncryptedSession().encrypt(password)) { - Pair response = collection.createItem(properties, secret, false).get(); - if (response == null) return null; - item = response.a; - if ("/".equals(item.getPath())) { - org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(response.b); - if (!completed.dismissed) { - org.freedesktop.secret.Collection.ItemCreated ic = collection - .getSignalHandler() - .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class); - item = ic.item; - } - } - } catch (NoSuchAlgorithmException | - NoSuchPaddingException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not encrypt the secret.", e); - } - - if (null != item) { - return Optional.of(item.getPath()); - } else { - return Optional.empty(); - } + Optional result = session.getEncryptedSession().encrypt(password) + .flatMap(secret -> { + try (secret) { // auto-close + final Map properties = Item.createProperties(label, attributes); + return collection.createItem(properties, secret, false) + .flatMap(pair -> Optional.ofNullable(pair.a) + .map(item -> { + if ("/".equals(item.getPath())) { // prompt required + org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(pair.b); + if (completed.dismissed) { + return item; + } else { + return collection + .getSignalHandler() + .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class) + .item; + } + } else { + return item; + } + }) + .map(DBusPath::getPath)); + } + }); + return result; } @Override public boolean delete() { - return false; + if (!isDefault()) { + ObjectPath promptPath = collection.delete().get(); + performPrompt(promptPath); + } else { + log.error("Default collections may not be deleted with the simple API."); + return false; + } + return true; } @Override public boolean deleteItem(String objectPath) { - return false; + if (Static.Utils.isNullOrEmpty(objectPath)) { + log.error("Cannot delete an unspecified item."); + return false; + } + + unlockWithUserPermission(); + + Item item = getItem(objectPath).get(); + ObjectPath promptPath = item.delete().get(); + performPrompt(promptPath); + return true; } @Override public boolean deleteItems(List objectPaths) { - return false; + unlockWithUserPermission(); + + boolean allDeleted = true; + + for (String item : objectPaths) { + boolean success = deleteItem(item); + if (!success) { + allDeleted = false; + } + } + + return allDeleted; } @Override public Optional> getAttributes(String objectPath) { - return Optional.empty(); + if (Static.Utils.isNullOrEmpty(objectPath)) return null; + unlock(); + return getItem(objectPath).flatMap(item -> item.getAttributes()); } @Override public Optional> getItems(Map attributes) { - return Optional.empty(); + if (attributes == null) return Optional.empty(); + unlock(); + + return Optional.ofNullable(collection.searchItems(attributes)) + .filter(objects -> !objects.isEmpty()) + .flatMap(objects -> objects.map(Static.Convert::toStrings)); + } + + @Override + public Optional getItemLabel(String objectPath) { + if (Static.Utils.isNullOrEmpty(objectPath)) return Optional.empty(); + unlock(); + return getItem(objectPath) + .flatMap(item -> item.getLabel()); + } + + @Override + public boolean setItemLabel(String objectPath, String label) { + if (Static.Utils.isNullOrEmpty(objectPath)) return false; + unlock(); + return getItem(objectPath) + .map(item -> item.setLabel(label)) + .orElse(false); + } + + @Override + public boolean setLabel(String label) { + boolean success = collection.setLabel(label); + if (success) { + this.label = label; + } + return success; } @Override - public Optional getLabel(String objectPath) { - return Optional.empty(); + public Optional getLabel() { + return Optional.of(this.label); } @Override - public Optional setLabel(String objectPath) { - return Optional.empty(); + public Optional getId() { + return Optional.of(this.collection.getId()); } @Override @@ -168,55 +335,80 @@ public Optional getSecret(String objectPath) { if (Static.Utils.isNullOrEmpty(objectPath)) return Optional.empty(); unlock(); - final Item item = getItem(objectPath); - - char[] decrypted = null; - ObjectPath sessionPath = session.getSession().getPath(); - try (final Secret secret = item.getSecret(sessionPath).orElseGet(() -> new Secret(sessionPath, null))) { - decrypted = session.getEncryptedSession().decrypt(secret); - } catch (NoSuchPaddingException | - NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not decrypt the secret.", e); - return Optional.empty(); - } - if (decrypted == null) { - return Optional.empty(); - } else { - return Optional.of(decrypted); - } + return getItem(objectPath) + .flatMap(item -> { + ObjectPath sessionPath = session.getSession().getPath(); + return item.getSecret(sessionPath); + }) + .flatMap(secret -> { + try { + char[] decrypted = session.getEncryptedSession().decrypt(secret); + return Optional.of(decrypted); + } catch (BadPaddingException | + IllegalBlockSizeException | + InvalidAlgorithmParameterException | + InvalidKeyException | + NoSuchAlgorithmException | + NoSuchPaddingException e) { + log.error("Could not decrypt the secret.", e); + return Optional.empty(); + } finally { + secret.clear(); + } + }); } @Override public Optional> getSecrets() { - return Optional.empty(); + unlockWithUserPermission(); + + List items = collection.getItems().get(); + if (items == null) return null; + + Map passwords = new HashMap(); + for (ObjectPath item : items) { + String path = item.getPath(); + passwords.put(path, getSecret(path).get()); + } + + return Optional.of(passwords); } @Override public boolean isLocked() { - return false; + if (connection != null && connection.isConnected()) { + return collection.isLocked(); + } else { + log.error("No D-Bus connection: Cannot check if the collection is locked."); + return true; + } } @Override public boolean lock() { - return false; + if (collection != null && !collection.isLocked()) { + service.getService().lock(lockable()); + log.info("Locked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + try { + Thread.currentThread().sleep(DEFAULT_DELAY_MILLIS); + } catch (InterruptedException e) { + log.error("Unexpected interrupt while waiting for a collection to lock.", e); + } + } + return collection.isLocked(); } private void unlock() { if (collection != null && collection.isLocked()) { - if (encrypted == null || isDefault()) { + if (encryptedCollectionPassword.isEmpty() || isDefault()) { Pair, ObjectPath> response = service.getService().unlock(lockable()).get(); performPrompt(response.b); if (!collection.isLocked()) { isUnlockedOnceWithUserPermission = true; log.info("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); } - } else if (service.isOrgGnomeKeyringAvailable()) { - InternalUnsupportedGuiltRiddenInterface withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(service.getService()); - withoutPrompt.unlockWithMasterPassword(collection.getPath(), encrypted); + } else if (encryptedCollectionPassword.isPresent() && service.isOrgGnomeKeyringAvailable()) { + withoutPrompt.unlockWithMasterPassword(collection.getPath(), encryptedCollectionPassword.get()); log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); } } @@ -224,19 +416,56 @@ private void unlock() { @Override public boolean unlockWithUserPermission() { - return false; + if (!isUnlockedOnceWithUserPermission && isDefault()) lock(); + unlock(); + if (collection.isLocked()) { + log.error("The collection was not unlocked with user permission."); + return false; + } + return true; } @Override public boolean updateItem(String objectPath, String label, CharSequence password, Map attributes) { - return false; + + if (Static.Utils.isNullOrEmpty(objectPath)) { + log.error("The object path of the item may not be null or empty."); + return false; + } + + if (Static.Utils.isNullOrEmpty(password)) { + log.error("The password may not be null or empty."); + return false; + } + + unlock(); + + Item item = getItem(objectPath).get(); + + if (label != null) { + item.setLabel(label); + } + + if (attributes != null) { + item.setAttributes(attributes); + } + + return session.getEncryptedSession().encrypt(password) + .map(secret -> { + try (secret) { // auto-close + return item.setSecret(secret); // side-effect + } + }) + .orElse(false); } @Override public void close() throws Exception { - // TODO: remove log.info - log.info("collection close is triggered"); - if (this.encrypted != null) this.encrypted.close(); + if (encryptedCollectionPassword.isPresent()) { + encryptedCollectionPassword.get().close(); + log.trace("cleared collection password"); + } + log.trace("closed collection"); } private Map getLabels() { @@ -244,7 +473,7 @@ private Map getLabels() { Map labels = new HashMap(); for (ObjectPath path : collections) { - org.freedesktop.secret.Collection c = new org.freedesktop.secret.Collection(path, service.getService(), null); + org.freedesktop.secret.Collection c = new org.freedesktop.secret.Collection(path, connection, null); labels.put(path, c.getLabel().get()); } @@ -256,42 +485,11 @@ private boolean exists(String label) { return labels.containsValue(label); } - private ObjectPath getCollectionPath(String label) { - Map labels = getLabels(); - - ObjectPath path = null; - for (Map.Entry entry : labels.entrySet()) { - ObjectPath p = entry.getKey(); - String l = entry.getValue(); - if (label.equals(l)) { - path = p; - break; - } - } - return path; - } - - private boolean isDefault() { - if (connection != null && connection.isConnected()) { - List defaults = Arrays.asList(null, "login", "session", "default"); - return defaults.contains(collection.getId()); - } else { - log.error("No D-Bus connection: Cannot check if the collection is the default collection."); - return false; - } - } - - private void performPrompt(ObjectPath path) { - if (!("/".equals(path.getPath()))) { - prompt.await(path, timeout); - } - } - - private Item getItem(String path) { + private Optional getItem(String path) { if (path != null) { - return new Item(Static.Convert.toObjectPath(path), service.getService()); + return Optional.of(new Item(Static.Convert.toObjectPath(path), service.getService())); } else { - return null; + return Optional.empty(); } } diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java index 3aac555..f4e62e5 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -1,90 +1,26 @@ package de.swiesend.secret.functional; -import de.swiesend.secret.functional.interfaces.ServiceInterface; -import de.swiesend.secret.functional.interfaces.SessionInterface; -import de.swiesend.secret.functional.interfaces.SystemInterface; -import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.exceptions.DBusException; -import org.freedesktop.dbus.interfaces.DBus; +import de.swiesend.secret.functional.interfaces.*; import org.freedesktop.secret.Pair; import org.freedesktop.secret.Service; -import org.freedesktop.secret.Static; -import org.freedesktop.secret.TransportEncryption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.*; -import java.util.Collection; +import static org.freedesktop.secret.Static.DEFAULT_PROMPT_TIMEOUT; -enum Activatable { - DBUS(Static.DBus.Service.DBUS), - SECRETS(Static.Service.SECRETS), - GNOME_KEYRING(org.gnome.keyring.Static.Service.KEYRING); - - public final String name; - - Activatable(String name) { - this.name = name; - } - -} - -class AvailableServices { - - private static final Logger log = LoggerFactory.getLogger(AvailableServices.class); - - public EnumSet services = EnumSet.noneOf(Activatable.class); - - public AvailableServices(System system) { - DBusConnection connection = system.getConnection(); - if (connection.isConnected()) { - try { - DBus bus = connection.getRemoteObject( - Static.DBus.Service.DBUS, - Static.DBus.ObjectPaths.DBUS, - DBus.class); - List activatableServices = Arrays.asList(bus.ListActivatableNames()); - - if (!activatableServices.contains(Static.DBus.Service.DBUS)) { - log.error("Missing D-Bus service: " + Static.DBus.Service.DBUS); - } else { - services.add(Activatable.DBUS); - } - - if (!activatableServices.contains(Static.Service.SECRETS)) { - log.error("Missing D-Bus service: " + Static.Service.SECRETS); - } else { - services.add(Activatable.SECRETS); - } - if (!activatableServices.contains(org.gnome.keyring.Static.Service.KEYRING)) { - log.warn("Proceeding without D-Bus service: " + org.gnome.keyring.Static.Service.KEYRING); - } else { - services.add(Activatable.GNOME_KEYRING); - } - } catch (DBusException | ExceptionInInitializerError e) { - log.warn("The secret service is not available. You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); - } - } - } - - -} public class SecretService extends ServiceInterface { private static final Logger log = LoggerFactory.getLogger(SecretService.class); private Map sessions = new HashMap<>(); - private org.freedesktop.secret.Service service = null; - - // TODO: remove unnecessary fields - // private Prompt prompt = null; - // private InternalUnsupportedGuiltRiddenInterface withoutPrompt = null; - // private org.freedesktop.secret.Session session = null; + private org.freedesktop.secret.Service service; - private boolean gnomeKeyringAvailable = false; + private boolean gnomeKeyringAvailable; + private Duration timeout = DEFAULT_PROMPT_TIMEOUT; private SecretService(SystemInterface system, AvailableServices available) { this.service = new Service(system.getConnection()); @@ -101,53 +37,6 @@ public static Optional create() { .map(pair -> new SecretService(pair.a, pair.b)); } - /** - * Checks if all necessary D-Bus services are provided by the system:
- * org.freedesktop.DBus
- * org.freedesktop.secrets
- * org.gnome.keyring - * - * @return true if the secret service is available, otherwise false and will log an error message. - */ - private static boolean isAvailable(System system, AvailableServices available) { - DBusConnection connection = system.getConnection(); - if (connection.isConnected()) { - try { - if (!available.services.contains(Activatable.DBUS)) { - log.error("Missing D-Bus service: " + Activatable.DBUS.name); - return false; - } - if (!available.services.contains(Activatable.SECRETS)) { - log.error("Missing D-Bus service: " + Activatable.SECRETS.name); - return false; - } - if (!available.services.contains(Activatable.GNOME_KEYRING)) { - log.warn("Proceeding without D-Bus service: " + Activatable.GNOME_KEYRING.name); - } - - // The following calls intent to open a session without actually generating a full session. - // Necessary in order to check if the provided 'secret service' supports the expected transport - // encryption algorithm (DH_IETF1024_SHA256_AES128_CBC_PKCS7) or raises an error, like - // "org.freedesktop.DBus.Error.ServiceUnknown <: org.freedesktop.dbus.exceptions.DBusException" - TransportEncryption transport = new TransportEncryption(connection); - boolean isSessionSupported = transport - .initialize() - .flatMap(init -> init.openSession()) - .isPresent(); - transport.close(); - - return isSessionSupported; - } catch (ExceptionInInitializerError e) { - log.warn("The secret service is not available. " + - "You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); - return false; - } - } else { - log.error("No D-Bus connection: Cannot check if all needed services are available."); - return false; - } - } - @Override public boolean isOrgGnomeKeyringAvailable() { return this.gnomeKeyringAvailable; @@ -155,6 +44,7 @@ public boolean isOrgGnomeKeyringAvailable() { @Override public boolean clear() { + // TODO: implement return false; } @@ -182,29 +72,25 @@ public List getSessions() { return this.sessions.values().stream().toList(); } - /*@Override - public SystemInterface getSystem() { - return this.system; - }*/ - @Override public Duration getTimeout() { - return null; + return timeout; } @Override public void setTimeout(Duration timeout) { - + this.timeout = timeout; } @Override public void close() throws Exception { - // TODO: remove log.info - log.info("service close is triggered"); - List values = this.sessions.values().stream().toList(); - for (SessionInterface session : values) { - unregisterSession(session); - session.close(); + this.clear(); + List values = getSessions(); + if (values != null) { + for (SessionInterface session : values) { + unregisterSession(session); + session.close(); + } } } diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java index 7e2a646..25db096 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -73,8 +73,8 @@ public boolean clear() { } @Override - public Optional collection(String label, CharSequence password) { - CollectionInterface collection = new Collection(this, label, password); + public Optional collection(String label, Optional maybePassword) { + CollectionInterface collection = new Collection(this, label, maybePassword); this.collections.add(collection); return Optional.of(collection); } diff --git a/src/main/java/de/swiesend/secret/functional/System.java b/src/main/java/de/swiesend/secret/functional/System.java index 18b7284..881e1ff 100644 --- a/src/main/java/de/swiesend/secret/functional/System.java +++ b/src/main/java/de/swiesend/secret/functional/System.java @@ -2,6 +2,7 @@ import de.swiesend.secret.functional.interfaces.SystemInterface; import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +16,7 @@ public class System extends SystemInterface { private static DBusConnection connection = null; private System(DBusConnection connection) { - this.connection = connection; + System.connection = connection; } /** @@ -25,15 +26,10 @@ private System(DBusConnection connection) { */ public static Optional connect() { try { - DBusConnection dbus = DBusConnection.newConnection(DBusConnection.DBusBusType.SESSION); - System c = new System(dbus); - return Optional.of(c); + DBusConnection dbus = DBusConnectionBuilder.forSessionBus().build(); + return Optional.of(new System(dbus)); } catch (DBusException e) { - if (e == null) { - log.warn("Could not communicate properly with the D-Bus."); - } else { - log.warn(String.format("Could not communicate properly with the D-Bus: [%s]: %s", e.getClass().getSimpleName(), e.getMessage())); - } + log.warn(String.format("Could not communicate properly with the D-Bus: [%s]: %s", e.getClass().getSimpleName(), e.getMessage())); } return Optional.empty(); } @@ -53,12 +49,9 @@ public DBusConnection getConnection() { synchronized public boolean disconnect() { connection.disconnect(); - return connection.isConnected(); + return !connection.isConnected(); } - /*private static Optional setupShutdownHook() { - return Optional.empty(); - }*/ @Override public void close() throws Exception { connection.close(); From 687cab4f1fda0c2a4cc48131e01742e94d385e38 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:08:50 +0200 Subject: [PATCH 21/74] Add a basic functional test --- .../secret/functional/SecretServiceTest.java | 103 ++++++++++++++++-- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java index 7827170..5eaed4d 100644 --- a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java +++ b/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java @@ -1,40 +1,73 @@ package de.swiesend.secret.functional; +import de.swiesend.secret.functional.interfaces.AvailableServices; import de.swiesend.secret.functional.interfaces.CollectionInterface; import de.swiesend.secret.functional.interfaces.ServiceInterface; import de.swiesend.secret.functional.interfaces.SessionInterface; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public class SecretServiceTest { private static final Logger log = LoggerFactory.getLogger(SecretServiceTest.class); + private SecretService secretService = null; + + @BeforeEach + void beforeEach() { + secretService = (SecretService) SecretService.create().get(); + } + + @AfterEach + void afterEach() throws Exception { + secretService.close(); + } + @Test @DisplayName("Fiddling around") public void fiddlingAround() throws Exception { char[] password; try (ServiceInterface service = SecretService.create().get()) { - CollectionInterface collection = service.openSession().flatMap(session -> - session.collection("test", "test")).get(); + CollectionInterface collection = service + .openSession() + //.flatMap(session -> session.collection("täst", Optional.of("test"))) + .flatMap(session -> session.collection("täst", Optional.empty())) + //.flatMap(session -> session.defaultCollection()) + .get(); + String collectionLabel = collection.getLabel().get(); + String collectionId = collection.getId().get(); + log.info(String.format("Collection {label: %s, id: %s}", collectionLabel, collectionId)); + Map attributes = Map.of("key", "value"); String item = collection.createItem("label", "password", attributes).get(); password = collection.getSecret(item).get(); - /*for (String i : collection.getItems(attributes).get()) { - log.info("[" + collection.getLabel(item).get() + "]" + i); - // collection.deleteItem(i); - }*/ - // collection.delete(); - - CollectionInterface col2 = service.openSession().flatMap(session -> - session.collection("test", "test")).get(); + + for (String itemPath : collection.getItems(attributes).get()) { + String label = collection.getItemLabel(itemPath).get(); + log.info(String.format("[%s] {label: %s, attributes: %s}", itemPath, label, attributes)); + collection.deleteItem(itemPath); + } + // WARN: be careful activating this on the default collection... + if (collectionId != "default") { + log.info("Non default collection. Deleting collection..."); + // collection.delete(); + } + + // CollectionInterface col2 = service.openSession() + // .flatMap(session -> session.collection("test", "test")) + // .get(); } assertEquals("password", new String(password)); password = "".toCharArray(); @@ -77,4 +110,52 @@ public void fiddlingAround() throws Exception { }*/ } + @Test + void create() { + assertNotNull(secretService); + } + + @Test + void isOrgGnomeKeyringAvailable() { + System system = System.connect().get(); + assertTrue(SecretService.isAvailable(system, new AvailableServices(system))); + } + + // TODO: check if needed at all + @Test + void clear() { + } + + @Test + void openSession() { + assertTrue(secretService.openSession().isPresent()); + } + + @Test + void getSessions() { + assertEquals(0, secretService.getSessions().size()); + SessionInterface s1 = secretService.openSession().get(); + SessionInterface s2 = secretService.openSession().get(); + List actualSessions = secretService.getSessions().stream().toList(); + assertEquals(2, actualSessions.size()); + List actualSessionIds = actualSessions.stream().map(s -> s.getId()).toList(); + assertTrue(actualSessionIds.contains(s1.getId())); + assertTrue(actualSessionIds.contains(s2.getId())); + } + + @Test + void getTimeout() { + } + + @Test + void setTimeout() { + } + + @Test + void close() { + } + + @Test + void getService() { + } } From eca83027cba4c055cc33daed1abacfccc7a0b988 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:10:27 +0200 Subject: [PATCH 22/74] Add abstraction layer for detecting available D-Bus services --- .../functional/interfaces/Activatable.java | 16 ++++++ .../interfaces/AvailableServices.java | 54 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java b/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java new file mode 100644 index 0000000..a1fefa3 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java @@ -0,0 +1,16 @@ +package de.swiesend.secret.functional.interfaces; + +import org.freedesktop.secret.Static; + +public enum Activatable { + DBUS(Static.DBus.Service.DBUS), + SECRETS(Static.Service.SECRETS), + GNOME_KEYRING(org.gnome.keyring.Static.Service.KEYRING); + + public final String name; + + Activatable(String name) { + this.name = name; + } + +} diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java b/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java new file mode 100644 index 0000000..aa08547 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java @@ -0,0 +1,54 @@ +package de.swiesend.secret.functional.interfaces; + +import de.swiesend.secret.functional.System; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBus; +import org.freedesktop.secret.Static; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; + +public class AvailableServices { + + private static final Logger log = LoggerFactory.getLogger(AvailableServices.class); + + public EnumSet services = EnumSet.noneOf(Activatable.class); + + public AvailableServices(System system) { + DBusConnection connection = system.getConnection(); + if (connection.isConnected()) { + try { + DBus bus = connection.getRemoteObject( + Static.DBus.Service.DBUS, + Static.DBus.ObjectPaths.DBUS, + DBus.class); + List activatableServices = Arrays.asList(bus.ListActivatableNames()); + + if (!activatableServices.contains(Static.DBus.Service.DBUS)) { + log.error("Missing D-Bus service: " + Static.DBus.Service.DBUS); + } else { + services.add(Activatable.DBUS); + } + + if (!activatableServices.contains(Static.Service.SECRETS)) { + log.error("Missing D-Bus service: " + Static.Service.SECRETS); + } else { + services.add(Activatable.SECRETS); + } + if (!activatableServices.contains(org.gnome.keyring.Static.Service.KEYRING)) { + log.warn("Proceeding without D-Bus service: " + org.gnome.keyring.Static.Service.KEYRING); + } else { + services.add(Activatable.GNOME_KEYRING); + } + } catch (DBusException | ExceptionInInitializerError e) { + log.warn("The secret service is not available. You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); + } + } + } + + +} From 1d5c95d15356de2642d7b581366bc2e5ead8ab1e Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:14:29 +0200 Subject: [PATCH 23/74] Add basic unit test for D-Bus System connection management --- .../secret/functional/SystemTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/test/java/de/swiesend/secret/functional/SystemTest.java diff --git a/src/test/java/de/swiesend/secret/functional/SystemTest.java b/src/test/java/de/swiesend/secret/functional/SystemTest.java new file mode 100644 index 0000000..739b678 --- /dev/null +++ b/src/test/java/de/swiesend/secret/functional/SystemTest.java @@ -0,0 +1,48 @@ +package de.swiesend.secret.functional; + +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class SystemTest { + + System system; + + @BeforeEach + void beforeEach() { + system = System.connect().orElseThrow(); + } + + @AfterEach + void afterEach() { + assertDoesNotThrow(() -> system.close()); + } + + @Test + void connect() { + assertNotNull(system); + } + + @Test + void isConnected() { + assertTrue(System.isConnected()); + } + + @Test + void disconnect() { + assertTrue(system.getConnection().isConnected()); + assertTrue(system.disconnect()); + assertFalse(system.getConnection().isConnected()); + } + + @Test + void getConnection() { + DBusConnection connection = system.getConnection(); + assertNotNull(connection); + assertTrue(connection.isConnected()); + } + +} \ No newline at end of file From 4b0cc96108ef5aa6de9f3e3921badef0033639a5 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:24:07 +0200 Subject: [PATCH 24/74] Add high-level documentation --- .../java/de/swiesend/secret/functional/Collection.java | 3 +++ .../java/de/swiesend/secret/functional/SecretService.java | 4 +++- src/main/java/de/swiesend/secret/functional/Session.java | 3 +++ src/main/java/de/swiesend/secret/functional/System.java | 7 +++++-- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index a3dcd8c..288f104 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -23,6 +23,9 @@ import static org.freedesktop.secret.Static.DBus.DEFAULT_DELAY_MILLIS; +/** + * Representation of a Secret-Service collection. Main interface to interact with the keyring. Guarantees a valid Secret-Service session. + */ public class Collection implements CollectionInterface { private static final Logger log = LoggerFactory.getLogger(Collection.class); diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java index f4e62e5..4505ae9 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -11,7 +11,9 @@ import static org.freedesktop.secret.Static.DEFAULT_PROMPT_TIMEOUT; - +/** + * Entrypoint for high-level API. Manages Secret-Service sessions and the D-Bus connection. + */ public class SecretService extends ServiceInterface { private static final Logger log = LoggerFactory.getLogger(SecretService.class); diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java index 25db096..76da77c 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -13,6 +13,9 @@ import java.util.Optional; import java.util.UUID; +/** + * Manages a Secret-Service session on top of a D-Bus session. + */ public class Session implements SessionInterface { private static final Logger log = LoggerFactory.getLogger(Session.class); diff --git a/src/main/java/de/swiesend/secret/functional/System.java b/src/main/java/de/swiesend/secret/functional/System.java index 881e1ff..2da5457 100644 --- a/src/main/java/de/swiesend/secret/functional/System.java +++ b/src/main/java/de/swiesend/secret/functional/System.java @@ -9,6 +9,9 @@ import java.util.Optional; +/** + * Manages the D-Bus connection on the system. + */ public class System extends SystemInterface { private static final Logger log = LoggerFactory.getLogger(System.class); @@ -20,9 +23,9 @@ private System(DBusConnection connection) { } /** - * Try to get a new DBus connection. + * Try to get a new D-Bus connection. * - * @return a new DBusConnection or Optional.empty() + * @return a new `DBusConnection` or `Optional.empty()` */ public static Optional connect() { try { From ed689484e9657a7fcc46783b12c4d671ca900557 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 26 Jun 2023 10:08:25 +0200 Subject: [PATCH 25/74] Add functional interfaces --- .../secret/functional/Collection.java | 24 ++--- .../secret/functional/SecretService.java | 10 +- .../swiesend/secret/functional/Session.java | 6 +- .../functional/interfaces/Activatable.java | 4 +- .../interfaces/AvailableServices.java | 6 +- .../interfaces/CollectionInterface.java | 47 ++++++++ .../interfaces/ServiceInterface.java | 102 ++++++++++++++++++ .../interfaces/SessionInterface.java | 27 +++++ 8 files changed, 201 insertions(+), 25 deletions(-) create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java create mode 100644 src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index 288f104..c7f2282 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -3,12 +3,12 @@ import de.swiesend.secret.functional.interfaces.CollectionInterface; import de.swiesend.secret.functional.interfaces.ServiceInterface; import de.swiesend.secret.functional.interfaces.SessionInterface; +import de.swiesend.secretservice.*; +import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.types.Variant; -import org.freedesktop.secret.*; -import org.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +21,7 @@ import java.time.Duration; import java.util.*; -import static org.freedesktop.secret.Static.DBus.DEFAULT_DELAY_MILLIS; +import static de.swiesend.secretservice.Static.DBus.DEFAULT_DELAY_MILLIS; /** * Representation of a Secret-Service collection. Main interface to interact with the keyring. Guarantees a valid Secret-Service session. @@ -30,7 +30,7 @@ public class Collection implements CollectionInterface { private static final Logger log = LoggerFactory.getLogger(Collection.class); - org.freedesktop.secret.Collection collection = null; + de.swiesend.secretservice.Collection collection = null; SessionInterface session = null; ServiceInterface service = null; DBusConnection connection = null; @@ -48,7 +48,7 @@ public Collection(SessionInterface session) { init(session); this.path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); - this.collection = new org.freedesktop.secret.Collection(path, connection); + this.collection = new de.swiesend.secretservice.Collection(path, connection); this.label = collection.getLabel().get(); this.id = collection.getId(); } @@ -77,7 +77,7 @@ private void init(SessionInterface session) { } } - private Optional getOrCreateCollection(String label) { + private Optional getOrCreateCollection(String label) { Optional maybePath; if (exists(label)) { @@ -91,7 +91,7 @@ private Optional getOrCreateCollection(String private Optional createNewCollection(String label) { ObjectPath path = null; - Map properties = org.freedesktop.secret.Collection.createProperties(label); + Map properties = de.swiesend.secretservice.Collection.createProperties(label); if (encryptedCollectionPassword.isEmpty()) { path = createCollectionWithPrompt(properties); @@ -135,13 +135,13 @@ private ObjectPath createCollectionWithPrompt(Map properties) { return null; } - private Optional getCollectionFromPath(ObjectPath path, String label) { + private Optional getCollectionFromPath(ObjectPath path, String label) { if (path == null) { log.error(String.format("Could not acquire collection with label: \"%s\"", label)); return Optional.empty(); } - collection = new org.freedesktop.secret.Collection(path, connection); + collection = new de.swiesend.secretservice.Collection(path, connection); return Optional.of(collection); } @@ -218,13 +218,13 @@ public Optional createItem(String label, CharSequence password, Map Optional.ofNullable(pair.a) .map(item -> { if ("/".equals(item.getPath())) { // prompt required - org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(pair.b); + de.swiesend.secretservice.interfaces.Prompt.Completed completed = prompt.await(pair.b); if (completed.dismissed) { return item; } else { return collection .getSignalHandler() - .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class) + .getLastHandledSignal(de.swiesend.secretservice.Collection.ItemCreated.class) .item; } } else { @@ -476,7 +476,7 @@ private Map getLabels() { Map labels = new HashMap(); for (ObjectPath path : collections) { - org.freedesktop.secret.Collection c = new org.freedesktop.secret.Collection(path, connection, null); + de.swiesend.secretservice.Collection c = new de.swiesend.secretservice.Collection(path, connection, null); labels.put(path, c.getLabel().get()); } diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java index 4505ae9..f267c8d 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -1,15 +1,15 @@ package de.swiesend.secret.functional; import de.swiesend.secret.functional.interfaces.*; -import org.freedesktop.secret.Pair; -import org.freedesktop.secret.Service; +import de.swiesend.secretservice.Pair; +import de.swiesend.secretservice.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.*; -import static org.freedesktop.secret.Static.DEFAULT_PROMPT_TIMEOUT; +import static de.swiesend.secretservice.Static.DEFAULT_PROMPT_TIMEOUT; /** * Entrypoint for high-level API. Manages Secret-Service sessions and the D-Bus connection. @@ -18,7 +18,7 @@ public class SecretService extends ServiceInterface { private static final Logger log = LoggerFactory.getLogger(SecretService.class); private Map sessions = new HashMap<>(); - private org.freedesktop.secret.Service service; + private de.swiesend.secretservice.Service service; private boolean gnomeKeyringAvailable; @@ -96,7 +96,7 @@ public void close() throws Exception { } } - public Service getService() { + public de.swiesend.secretservice.Service getService() { return service; } } diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java index 76da77c..d7ac64c 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -3,8 +3,8 @@ import de.swiesend.secret.functional.interfaces.CollectionInterface; import de.swiesend.secret.functional.interfaces.ServiceInterface; import de.swiesend.secret.functional.interfaces.SessionInterface; -import org.freedesktop.secret.Service; -import org.freedesktop.secret.TransportEncryption; +import de.swiesend.secretservice.Service; +import de.swiesend.secretservice.TransportEncryption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +55,7 @@ public TransportEncryption.EncryptedSession getEncryptedSession() { } @Override - public org.freedesktop.secret.Session getSession() { + public de.swiesend.secretservice.Session getSession() { return this.encryptedSession.getSession(); } diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java b/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java index a1fefa3..e9bcf39 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java +++ b/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java @@ -1,11 +1,11 @@ package de.swiesend.secret.functional.interfaces; -import org.freedesktop.secret.Static; +import de.swiesend.secretservice.Static; public enum Activatable { DBUS(Static.DBus.Service.DBUS), SECRETS(Static.Service.SECRETS), - GNOME_KEYRING(org.gnome.keyring.Static.Service.KEYRING); + GNOME_KEYRING(de.swiesend.secretservice.gnome.keyring.Static.Service.KEYRING); public final String name; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java b/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java index aa08547..49393f0 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java +++ b/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java @@ -1,10 +1,10 @@ package de.swiesend.secret.functional.interfaces; import de.swiesend.secret.functional.System; +import de.swiesend.secretservice.Static; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBus; -import org.freedesktop.secret.Static; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,8 +39,8 @@ public AvailableServices(System system) { } else { services.add(Activatable.SECRETS); } - if (!activatableServices.contains(org.gnome.keyring.Static.Service.KEYRING)) { - log.warn("Proceeding without D-Bus service: " + org.gnome.keyring.Static.Service.KEYRING); + if (!activatableServices.contains(de.swiesend.secretservice.gnome.keyring.Static.Service.KEYRING)) { + log.warn("Proceeding without D-Bus service: " + de.swiesend.secretservice.gnome.keyring.Static.Service.KEYRING); } else { services.add(Activatable.GNOME_KEYRING); } diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java new file mode 100644 index 0000000..a27d6fb --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java @@ -0,0 +1,47 @@ +package de.swiesend.secret.functional.interfaces; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public interface CollectionInterface extends AutoCloseable { + + boolean clear(); + + Optional createItem(String label, CharSequence password); + + Optional createItem(String label, CharSequence password, Map attributes); + + boolean delete(); + + boolean deleteItem(String objectPath); + + boolean deleteItems(List objectPaths); + + Optional> getAttributes(String objectPath); + + Optional> getItems(Map attributes); + + Optional getItemLabel(String objectPath); + + boolean setItemLabel(String objectPath, String label); + + boolean setLabel(String label); + + Optional getLabel(); + + Optional getId(); + + Optional getSecret(String objectPath); + + Optional> getSecrets(); + + boolean isLocked(); + + boolean lock(); + + boolean unlockWithUserPermission(); + + boolean updateItem(String objectPath, String label, CharSequence password, Map attributes); + +} diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java new file mode 100644 index 0000000..b1205cf --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java @@ -0,0 +1,102 @@ +package de.swiesend.secret.functional.interfaces; + +import de.swiesend.secret.functional.System; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import de.swiesend.secretservice.TransportEncryption; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +public abstract class ServiceInterface implements AutoCloseable { + + private static final Logger log = LoggerFactory.getLogger(ServiceInterface.class); + + public static Optional create() { + log.warn("Do not call the interface method, but the implementation."); + return Optional.empty(); + } + + synchronized public static boolean disconnect() { + log.warn("Do not call the interface method, but the implementation."); + return false; + } + + private static Optional getConnection() { + log.warn("Do not call the interface method, but the implementation."); + return Optional.empty(); + } + + public static boolean isConnected() { + log.warn("Do not call the interface method, but the implementation."); + return false; + } + + private static Optional setupShutdownHook() { + log.warn("Do not call the interface method, but the implementation."); + return Optional.empty(); + } + + /** + * Checks if all necessary D-Bus services are provided by the system:
+ * org.freedesktop.DBus
+ * org.freedesktop.secrets
+ * org.gnome.keyring + * + * @return true if the secret service is available, otherwise false and will log an error message. + */ + public static boolean isAvailable(System system, AvailableServices available) { + DBusConnection connection = system.getConnection(); + if (connection.isConnected()) { + try { + if (!available.services.contains(Activatable.DBUS)) { + log.error("Missing D-Bus service: " + Activatable.DBUS.name); + return false; + } + if (!available.services.contains(Activatable.SECRETS)) { + log.error("Missing D-Bus service: " + Activatable.SECRETS.name); + return false; + } + if (!available.services.contains(Activatable.GNOME_KEYRING)) { + log.warn("Proceeding without D-Bus service: " + Activatable.GNOME_KEYRING.name); + } + + // The following calls intent to open a session without actually generating a full session. + // Necessary in order to check if the provided 'secret service' supports the expected transport + // encryption algorithm (DH_IETF1024_SHA256_AES128_CBC_PKCS7) or raises an error, like + // "org.freedesktop.DBus.Error.ServiceUnknown <: org.freedesktop.dbus.exceptions.DBusException" + TransportEncryption transport = new TransportEncryption(connection); + boolean isSessionSupported = transport + .initialize() + .flatMap(TransportEncryption.InitializedSession::openSession) + .isPresent(); + transport.close(); + + return isSessionSupported; + } catch (ExceptionInInitializerError e) { + log.warn("The secret service is not available. " + + "You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); + return false; + } + } else { + log.error("No D-Bus connection: Cannot check if all needed services are available."); + return false; + } + } + + abstract public boolean clear(); + + abstract public Optional openSession(); + + abstract public List getSessions(); + + abstract public Duration getTimeout(); + + abstract public void setTimeout(Duration timeout); + + abstract public de.swiesend.secretservice.Service getService(); + + abstract public boolean isOrgGnomeKeyringAvailable(); +} diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java new file mode 100644 index 0000000..d1df7f4 --- /dev/null +++ b/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java @@ -0,0 +1,27 @@ +package de.swiesend.secret.functional.interfaces; + +import de.swiesend.secretservice.TransportEncryption; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface SessionInterface extends AutoCloseable { + + boolean clear(); + + Optional collection(String label, Optional maybePassword); + + Optional defaultCollection(); + + TransportEncryption.EncryptedSession getEncryptedSession(); + + ServiceInterface getService(); + + de.swiesend.secretservice.Session getSession(); + + List getCollections(); + + UUID getId(); + +} From d22ee4e2015def1b6aa38e9657c4d902147a0edd Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 2 Jul 2023 14:36:35 +0200 Subject: [PATCH 26/74] Fix merge problems --- pom.xml | 6 +++--- src/main/java/de/swiesend/secretservice/Service.java | 2 ++ .../de/swiesend/secretservice/simple/SimpleCollection.java | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 80a1856..15247fc 100644 --- a/pom.xml +++ b/pom.xml @@ -261,9 +261,9 @@ maven-compiler-plugin 3.11.0 - 9 - 9 - 9 + 17 + 17 + 17 diff --git a/src/main/java/de/swiesend/secretservice/Service.java b/src/main/java/de/swiesend/secretservice/Service.java index 7de7e53..8c4dd96 100644 --- a/src/main/java/de/swiesend/secretservice/Service.java +++ b/src/main/java/de/swiesend/secretservice/Service.java @@ -5,6 +5,8 @@ import org.freedesktop.dbus.messages.DBusSignal; import org.freedesktop.dbus.types.Variant; import de.swiesend.secretservice.handlers.Messaging; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 56a2c1a..dc8745a 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -472,13 +472,13 @@ public String createItem(String label, CharSequence password, Map Optional.ofNullable(pair.a) .map(item -> { if ("/".equals(item.getPath())) { // prompt required - org.freedesktop.secret.interfaces.Prompt.Completed completed = prompt.await(pair.b); + de.swiesend.secretservice.interfaces.Prompt.Completed completed = prompt.await(pair.b); if (completed.dismissed) { return item; } else { return collection .getSignalHandler() - .getLastHandledSignal(org.freedesktop.secret.Collection.ItemCreated.class) + .getLastHandledSignal(de.swiesend.secretservice.Collection.ItemCreated.class) .item; } } else { From 4d87063fdbb94396ad6f9c697a06b6e4229ec720 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 2 Jul 2023 14:47:00 +0200 Subject: [PATCH 27/74] Remove log statement removes a TODO that need to be resolved with the new API returning Optional --- .../de/swiesend/secretservice/simple/SimpleCollection.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index dc8745a..44a56eb 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -193,11 +193,6 @@ public static boolean isAvailable() { log.warn("The secret service is not available. You may want to install the `gnome-keyring` package. Is the `gnome-keyring-daemon` running?", e); return false; } - // TODO: remove comment - /*catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) { - log.error("The secret service could not be initialized as the service does not provide the expected transport encryption algorithm.", e); - return false; - }*/ } else { log.error("No D-Bus connection: Cannot check if all needed services are available."); return false; From d86eb43aa69148a27c8fcd7a9acb76ead94e3072 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 2 Jul 2023 14:48:36 +0200 Subject: [PATCH 28/74] Remove log statement do not log the collection secret --- src/main/java/de/swiesend/secret/functional/Collection.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secret/functional/Collection.java index c7f2282..3680c0c 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secret/functional/Collection.java @@ -181,8 +181,6 @@ private void performPrompt(ObjectPath path) { public boolean clear() { if (encryptedCollectionPassword.isPresent()) { encryptedCollectionPassword.get().clear(); - // TODO: remove log statement - log.info("collection password: " + encryptedCollectionPassword.get().getSecretValue()); } return true; } From 8a92c472a82549240f6cea101c4c1351a65bd324 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 2 Jul 2023 14:51:01 +0200 Subject: [PATCH 29/74] Remove unused method from the SecretService interface --- .../java/de/swiesend/secret/functional/SecretService.java | 7 ------- .../secret/functional/interfaces/ServiceInterface.java | 4 +--- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secret/functional/SecretService.java index f267c8d..cd2c959 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secret/functional/SecretService.java @@ -44,12 +44,6 @@ public boolean isOrgGnomeKeyringAvailable() { return this.gnomeKeyringAvailable; } - @Override - public boolean clear() { - // TODO: implement - return false; - } - @Override public Optional openSession() { Optional session = Session @@ -86,7 +80,6 @@ public void setTimeout(Duration timeout) { @Override public void close() throws Exception { - this.clear(); List values = getSessions(); if (values != null) { for (SessionInterface session : values) { diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java index b1205cf..65b39d4 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java +++ b/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java @@ -1,8 +1,8 @@ package de.swiesend.secret.functional.interfaces; import de.swiesend.secret.functional.System; -import org.freedesktop.dbus.connections.impl.DBusConnection; import de.swiesend.secretservice.TransportEncryption; +import org.freedesktop.dbus.connections.impl.DBusConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,8 +86,6 @@ public static boolean isAvailable(System system, AvailableServices available) { } } - abstract public boolean clear(); - abstract public Optional openSession(); abstract public List getSessions(); From d71eea8eb1b4690e8c11bdfad931eeed14dcaf3e Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 2 Jul 2023 14:55:03 +0200 Subject: [PATCH 30/74] Remove unused method clear() from the functional Session interface --- src/main/java/de/swiesend/secret/functional/Session.java | 7 +------ .../secret/functional/interfaces/SessionInterface.java | 2 -- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secret/functional/Session.java index d7ac64c..59caf48 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secret/functional/Session.java @@ -69,12 +69,6 @@ public ServiceInterface getService() { return service; } - @Override - public boolean clear() { - // TODO: to be implemented - return false; - } - @Override public Optional collection(String label, Optional maybePassword) { CollectionInterface collection = new Collection(this, label, maybePassword); @@ -94,6 +88,7 @@ public void close() throws Exception { for (CollectionInterface collection : this.collections) { collection.close(); } + encryptedSession.getSession().close(); } @Override diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java b/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java index d1df7f4..df92ed5 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java +++ b/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java @@ -8,8 +8,6 @@ public interface SessionInterface extends AutoCloseable { - boolean clear(); - Optional collection(String label, Optional maybePassword); Optional defaultCollection(); From 46e14755c8cf7361da2ecb962f662c597e3ece78 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 16:41:32 +0200 Subject: [PATCH 31/74] Move functional packages to de.swiesend.secretservice.functional and optimize imports --- .../functional/Collection.java | 8 ++++---- .../functional/SecretService.java | 4 ++-- .../{secret => secretservice}/functional/Session.java | 8 ++++---- .../{secret => secretservice}/functional/System.java | 4 ++-- .../functional/interfaces/Activatable.java | 2 +- .../functional/interfaces/AvailableServices.java | 4 ++-- .../functional/interfaces/CollectionInterface.java | 2 +- .../functional/interfaces/ServiceInterface.java | 4 ++-- .../functional/interfaces/SessionInterface.java | 2 +- .../functional/interfaces/SystemInterface.java | 4 ++-- .../functional/SecretServiceTest.java | 10 +++++----- .../functional/SystemTest.java | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) rename src/main/java/de/swiesend/{secret => secretservice}/functional/Collection.java (98%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/SecretService.java (96%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/Session.java (91%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/System.java (93%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/Activatable.java (85%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/AvailableServices.java (95%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/CollectionInterface.java (95%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/ServiceInterface.java (97%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/SessionInterface.java (90%) rename src/main/java/de/swiesend/{secret => secretservice}/functional/interfaces/SystemInterface.java (88%) rename src/test/java/de/swiesend/{secret => secretservice}/functional/SecretServiceTest.java (94%) rename src/test/java/de/swiesend/{secret => secretservice}/functional/SystemTest.java (95%) diff --git a/src/main/java/de/swiesend/secret/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java similarity index 98% rename from src/main/java/de/swiesend/secret/functional/Collection.java rename to src/main/java/de/swiesend/secretservice/functional/Collection.java index 3680c0c..3be6cca 100644 --- a/src/main/java/de/swiesend/secret/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -1,9 +1,9 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; -import de.swiesend.secret.functional.interfaces.CollectionInterface; -import de.swiesend.secret.functional.interfaces.ServiceInterface; -import de.swiesend.secret.functional.interfaces.SessionInterface; import de.swiesend.secretservice.*; +import de.swiesend.secretservice.functional.interfaces.CollectionInterface; +import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import de.swiesend.secretservice.functional.interfaces.SessionInterface; import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; diff --git a/src/main/java/de/swiesend/secret/functional/SecretService.java b/src/main/java/de/swiesend/secretservice/functional/SecretService.java similarity index 96% rename from src/main/java/de/swiesend/secret/functional/SecretService.java rename to src/main/java/de/swiesend/secretservice/functional/SecretService.java index cd2c959..39ff839 100644 --- a/src/main/java/de/swiesend/secret/functional/SecretService.java +++ b/src/main/java/de/swiesend/secretservice/functional/SecretService.java @@ -1,8 +1,8 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; -import de.swiesend.secret.functional.interfaces.*; import de.swiesend.secretservice.Pair; import de.swiesend.secretservice.Service; +import de.swiesend.secretservice.functional.interfaces.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/de/swiesend/secret/functional/Session.java b/src/main/java/de/swiesend/secretservice/functional/Session.java similarity index 91% rename from src/main/java/de/swiesend/secret/functional/Session.java rename to src/main/java/de/swiesend/secretservice/functional/Session.java index 59caf48..f193283 100644 --- a/src/main/java/de/swiesend/secret/functional/Session.java +++ b/src/main/java/de/swiesend/secretservice/functional/Session.java @@ -1,10 +1,10 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; -import de.swiesend.secret.functional.interfaces.CollectionInterface; -import de.swiesend.secret.functional.interfaces.ServiceInterface; -import de.swiesend.secret.functional.interfaces.SessionInterface; import de.swiesend.secretservice.Service; import de.swiesend.secretservice.TransportEncryption; +import de.swiesend.secretservice.functional.interfaces.CollectionInterface; +import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import de.swiesend.secretservice.functional.interfaces.SessionInterface; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/de/swiesend/secret/functional/System.java b/src/main/java/de/swiesend/secretservice/functional/System.java similarity index 93% rename from src/main/java/de/swiesend/secret/functional/System.java rename to src/main/java/de/swiesend/secretservice/functional/System.java index 2da5457..7c2fb1d 100644 --- a/src/main/java/de/swiesend/secret/functional/System.java +++ b/src/main/java/de/swiesend/secretservice/functional/System.java @@ -1,6 +1,6 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; -import de.swiesend.secret.functional.interfaces.SystemInterface; +import de.swiesend.secretservice.functional.interfaces.SystemInterface; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder; import org.freedesktop.dbus.exceptions.DBusException; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/Activatable.java similarity index 85% rename from src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/Activatable.java index e9bcf39..bd70776 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/Activatable.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/Activatable.java @@ -1,4 +1,4 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; import de.swiesend.secretservice.Static; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java similarity index 95% rename from src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java index 49393f0..028cce2 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/AvailableServices.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java @@ -1,7 +1,7 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; -import de.swiesend.secret.functional.System; import de.swiesend.secretservice.Static; +import de.swiesend.secretservice.functional.System; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBus; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java similarity index 95% rename from src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java index a27d6fb..4818816 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/CollectionInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java @@ -1,4 +1,4 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; import java.util.List; import java.util.Map; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java similarity index 97% rename from src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java index 65b39d4..b7147d2 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/ServiceInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java @@ -1,7 +1,7 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; -import de.swiesend.secret.functional.System; import de.swiesend.secretservice.TransportEncryption; +import de.swiesend.secretservice.functional.System; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/SessionInterface.java similarity index 90% rename from src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/SessionInterface.java index df92ed5..b66e291 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/SessionInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/SessionInterface.java @@ -1,4 +1,4 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; import de.swiesend.secretservice.TransportEncryption; diff --git a/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java similarity index 88% rename from src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java rename to src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java index 829a7b8..b95d577 100644 --- a/src/main/java/de/swiesend/secret/functional/interfaces/SystemInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java @@ -1,6 +1,6 @@ -package de.swiesend.secret.functional.interfaces; +package de.swiesend.secretservice.functional.interfaces; -import de.swiesend.secret.functional.System; +import de.swiesend.secretservice.functional.System; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java similarity index 94% rename from src/test/java/de/swiesend/secret/functional/SecretServiceTest.java rename to src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java index 5eaed4d..ca2431a 100644 --- a/src/test/java/de/swiesend/secret/functional/SecretServiceTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java @@ -1,9 +1,9 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; -import de.swiesend.secret.functional.interfaces.AvailableServices; -import de.swiesend.secret.functional.interfaces.CollectionInterface; -import de.swiesend.secret.functional.interfaces.ServiceInterface; -import de.swiesend.secret.functional.interfaces.SessionInterface; +import de.swiesend.secretservice.functional.interfaces.AvailableServices; +import de.swiesend.secretservice.functional.interfaces.CollectionInterface; +import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import de.swiesend.secretservice.functional.interfaces.SessionInterface; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/src/test/java/de/swiesend/secret/functional/SystemTest.java b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java similarity index 95% rename from src/test/java/de/swiesend/secret/functional/SystemTest.java rename to src/test/java/de/swiesend/secretservice/functional/SystemTest.java index 739b678..3669027 100644 --- a/src/test/java/de/swiesend/secret/functional/SystemTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java @@ -1,4 +1,4 @@ -package de.swiesend.secret.functional; +package de.swiesend.secretservice.functional; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.junit.jupiter.api.AfterEach; From d4082f2df3bd5bdcaafa4e2abddf3a234d3489d6 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 17:29:40 +0200 Subject: [PATCH 32/74] Let decrypt return an Optional --- .../secretservice/TransportEncryption.java | 32 ++++++++++--------- .../secretservice/functional/Collection.java | 17 ++-------- .../simple/SimpleCollection.java | 23 +++---------- .../integration/IntegrationTest.java | 4 +-- .../secretservice/integration/ItemTest.java | 15 ++------- 5 files changed, 28 insertions(+), 63 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index e0559f3..810e459 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -224,26 +224,28 @@ public Optional encrypt(byte[] plain, Charset charset) { } // TODO: return Optional; remove exceptions; - public char[] decrypt(Secret secret) throws - NoSuchPaddingException, - NoSuchAlgorithmException, - InvalidAlgorithmParameterException, - InvalidKeyException, - BadPaddingException, - IllegalBlockSizeException { + public Optional decrypt(Secret secret) { - if (secret == null) return null; + if (secret == null) return Optional.empty(); if (sessionKey == null) { - throw new IllegalStateException("Missing session key. Call Opened.generateSessionKey() first."); + log.error("Missing session key. Call Opened.generateSessionKey() first."); } - - IvParameterSpec ivSpec = new IvParameterSpec(secret.getSecretParameters()); - Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); - cipher.init(Cipher.DECRYPT_MODE, sessionKey, ivSpec); - final byte[] decrypted = cipher.doFinal(secret.getSecretValue()); + byte[] decrypted = new byte[0]; // TODO: should this be final? How to handle the final Secret.clear? try { - return Secret.toChars(decrypted); + IvParameterSpec ivSpec = new IvParameterSpec(secret.getSecretParameters()); + Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); + cipher.init(Cipher.DECRYPT_MODE, sessionKey, ivSpec); + decrypted = cipher.doFinal(secret.getSecretValue()); + return Optional.of(Secret.toChars(decrypted)); + } catch (InvalidAlgorithmParameterException | + InvalidKeyException | + NoSuchPaddingException | + BadPaddingException | + IllegalBlockSizeException | + NoSuchAlgorithmException e) { + log.error("Could not decrypt the secret", e); + return Optional.empty(); } finally { Secret.clear(decrypted); } diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 3be6cca..5ac368f 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -342,20 +342,9 @@ public Optional getSecret(String objectPath) { return item.getSecret(sessionPath); }) .flatMap(secret -> { - try { - char[] decrypted = session.getEncryptedSession().decrypt(secret); - return Optional.of(decrypted); - } catch (BadPaddingException | - IllegalBlockSizeException | - InvalidAlgorithmParameterException | - InvalidKeyException | - NoSuchAlgorithmException | - NoSuchPaddingException e) { - log.error("Could not decrypt the secret.", e); - return Optional.empty(); - } finally { - secret.clear(); - } + Optional decrypted = session.getEncryptedSession().decrypt(secret); // TODO: should this be final? + secret.clear(); + return decrypted; }); } diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 44a56eb..0e24816 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -1,6 +1,8 @@ package de.swiesend.secretservice.simple; +import de.swiesend.secretservice.Collection; import de.swiesend.secretservice.*; +import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.connections.impl.DBusConnection; @@ -8,20 +10,11 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBus; import org.freedesktop.dbus.types.Variant; -import de.swiesend.secretservice.Collection; -import de.swiesend.secretservice.interfaces.Prompt.Completed; -import de.swiesend.secretservice.gnome.keyring.InternalUnsupportedGuiltRiddenInterface; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.io.IOException; import java.security.AccessControlException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.*; import java.util.concurrent.RejectedExecutionException; @@ -599,19 +592,11 @@ public char[] getSecret(String objectPath) { final Item item = getItem(objectPath); - char[] decrypted = null; + Optional decrypted = null; ObjectPath sessionPath = session.getPath(); try (final Secret secret = item.getSecret(sessionPath).orElseGet(() -> new Secret(sessionPath, null))) { - decrypted = transportEncryptedSession.decrypt(secret); - } catch (NoSuchPaddingException | - NoSuchAlgorithmException | - InvalidAlgorithmParameterException | - InvalidKeyException | - BadPaddingException | - IllegalBlockSizeException e) { - log.error("Could not decrypt the secret.", e); + return transportEncryptedSession.decrypt(secret).orElse(null); } - return decrypted; } /** diff --git a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java index 9e0f938..a27cc44 100644 --- a/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/IntegrationTest.java @@ -57,7 +57,7 @@ public void testWithTransportEncryption() throws byte[] encBase64 = Base64.getEncoder().encode(encrypted.getSecretValue()); log.info(label("encrypted secret (base64)", new String(encBase64))); - char[] decrypted = encryptedSession.decrypt(encrypted); + char[] decrypted = encryptedSession.decrypt(encrypted).get(); log.info(label(" decrypted secret", new String(decrypted))); assertEquals(plain, new String(decrypted)); @@ -104,7 +104,7 @@ public void testWithTransportEncryption() throws assertEquals(encrypted.getSession(), actual.getSession()); assertEquals(encrypted.getContentType(), actual.getContentType()); - decrypted = encryptedSession.decrypt(actual); + decrypted = encryptedSession.decrypt(actual).get(); log.info(label(" decrypted remote secret", new String(decrypted))); assertEquals(plain, new String(decrypted)); diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index 6548537..58ab23b 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -1,21 +1,15 @@ package de.swiesend.secretservice.integration; import de.swiesend.secretservice.*; +import de.swiesend.secretservice.integration.test.Context; import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.types.UInt64; -import de.swiesend.secretservice.integration.test.Context; import org.junit.jupiter.api.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.nio.charset.StandardCharsets; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -69,12 +63,7 @@ public void getSecret() { if (context.encrypted == false) { value = new String(secret.getSecretValue(), StandardCharsets.UTF_8); } else { - try { - value = new String(context.encryption.decrypt(secret)); - } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | - InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { - throw new RuntimeException(e); - } + value = new String(context.encryption.decrypt(secret).get()); } log.info(label("value", value)); assertEquals("super secret", value); From 3487006b054772cd3bc578ab0492581537c4d472 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 17:29:56 +0200 Subject: [PATCH 33/74] Fix createItem() --- .../java/de/swiesend/secretservice/simple/SimpleCollection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java index 0e24816..6d0c379 100644 --- a/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java +++ b/src/main/java/de/swiesend/secretservice/simple/SimpleCollection.java @@ -447,7 +447,7 @@ public String createItem(String label, CharSequence password, Map Date: Thu, 27 Jul 2023 17:47:05 +0200 Subject: [PATCH 34/74] Fix Item.setSecret() --- .../de/swiesend/secretservice/integration/ItemTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java index 58ab23b..34db81d 100644 --- a/src/test/java/de/swiesend/secretservice/integration/ItemTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/ItemTest.java @@ -90,12 +90,11 @@ public void getForeignSecret() { @Test public void setSecret() { - Secret secret = new Secret(context.session.getPath(), "new secret".getBytes()); - context.item.setSecret(secret); - + Secret secret = context.encryption.encrypt("new secret").get(); + assertTrue(context.item.setSecret(secret)); Secret result = context.item.getSecret(context.session.getPath()).get(); log.info(label("secret", result.toString())); - assertEquals("new secret", Static.Convert.toString(result.getSecretValue())); + assertEquals("new secret", new String(context.encryption.decrypt(result).orElse(null))); } @Test From 6fc84c6a2ec8090ad696ebfc23ceaba7e84430d7 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 18:03:55 +0200 Subject: [PATCH 35/74] Handle "org.freedesktop.DBus.Error.Failed" and multiple parameters --- .../handlers/MessageHandler.java | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 598b7a9..89962d6 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -1,10 +1,10 @@ package de.swiesend.secretservice.handlers; +import de.swiesend.secretservice.Static; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.messages.MethodCall; import org.freedesktop.dbus.types.Variant; -import de.swiesend.secretservice.Static; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,7 +57,11 @@ public Optional send(String service, String path, String iface, String if (Static.Utils.isNullOrEmpty(parameters)) { log.warn(error); } else { - log.warn(error + ": " + parameters[0]); + if (parameters.length == 1) { + log.warn(error + ": \"" + parameters[0] + "\""); + } else { + log.warn(error + ": " + Arrays.deepToString(parameters)); + } } return Optional.empty(); case "org.gnome.keyring.Error.Denied": @@ -65,7 +69,11 @@ public Optional send(String service, String path, String iface, String if (Static.Utils.isNullOrEmpty(parameters)) { log.info(error); } else { - log.info(error + ": " + parameters[0]); + if (parameters.length == 1) { + log.info(error + ": \"" + parameters[0] + "\""); + } else { + log.info(error + ": " + Arrays.deepToString(parameters)); + } } return Optional.empty(); case "org.freedesktop.DBus.Error.NoReply": @@ -73,7 +81,12 @@ public Optional send(String service, String path, String iface, String case "org.freedesktop.DBus.Error.UnknownMethod": case "org.freedesktop.DBus.Error.UnknownObject": case "org.freedesktop.DBus.Error.InvalidArgs": - log.error(error + ": " + Arrays.deepToString(parameters)); + case "org.freedesktop.DBus.Error.Failed": + if (parameters.length == 1) { + log.error(error + ": \"" + parameters[0] + "\""); + } else { + log.error(error + ": " + Arrays.deepToString(parameters)); + } case "org.freedesktop.DBus.Local.Disconnected": case "org.freedesktop.dbus.exceptions.FatalDBusException": case "org.freedesktop.dbus.exceptions.NotConnected": From 7657a9e2c7538c56f43991fd521a7926b0b73f1b Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 18:04:11 +0200 Subject: [PATCH 36/74] Fix createItem() --- .../swiesend/secretservice/integration/CollectionTest.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java index 1375972..0984ee5 100644 --- a/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/CollectionTest.java @@ -65,11 +65,7 @@ public void searchItems() { @Test public void createItem() { - - // some empty cipher parameters - byte[] parameters = "".getBytes(); - byte[] value = "super secret".getBytes(); - Secret secret = new Secret(context.session.getPath(), parameters, value); + Secret secret = context.encryption.encrypt("super secret").get(); Map attributes = new HashMap(); attributes.put("Attribute1", "Value1"); From 1ac023b80aa896557a8b3024d02a97554091e2d1 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 18:08:17 +0200 Subject: [PATCH 37/74] Disable disconnect() test single test is running fine, but test all is not due to timing issues --- .../java/de/swiesend/secretservice/functional/SystemTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/swiesend/secretservice/functional/SystemTest.java b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java index 3669027..3651238 100644 --- a/src/test/java/de/swiesend/secretservice/functional/SystemTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java @@ -3,6 +3,7 @@ import org.freedesktop.dbus.connections.impl.DBusConnection; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; @@ -32,7 +33,8 @@ void isConnected() { } @Test - void disconnect() { + @Disabled + void disconnect() throws InterruptedException { assertTrue(system.getConnection().isConnected()); assertTrue(system.disconnect()); assertFalse(system.getConnection().isConnected()); From 3b398330ac2c8fac2099f98a0745a720e123fde3 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 19:00:54 +0200 Subject: [PATCH 38/74] Fix MessageHandler switch --- .../java/de/swiesend/secretservice/handlers/MessageHandler.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 89962d6..fe26e35 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -87,6 +87,7 @@ public Optional send(String service, String path, String iface, String } else { log.error(error + ": " + Arrays.deepToString(parameters)); } + return Optional.empty(); case "org.freedesktop.DBus.Local.Disconnected": case "org.freedesktop.dbus.exceptions.FatalDBusException": case "org.freedesktop.dbus.exceptions.NotConnected": From ae6982136ff6a54f79d633c97cf2603bc84bc655 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 19:03:36 +0200 Subject: [PATCH 39/74] Return true in an array when no error or other parameters are present --- .../InternalUnsupportedGuiltRiddenInterface.java | 14 +++++--------- .../secretservice/handlers/MessageHandler.java | 7 ++++++- ...nternalUnsupportedGuiltRiddenInterfaceTest.java | 10 +++++++--- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java b/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java index 3234933..8ce8a85 100644 --- a/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java +++ b/src/main/java/de/swiesend/secretservice/gnome/keyring/InternalUnsupportedGuiltRiddenInterface.java @@ -1,12 +1,12 @@ package de.swiesend.secretservice.gnome.keyring; -import org.freedesktop.dbus.DBusPath; -import org.freedesktop.dbus.ObjectPath; -import org.freedesktop.dbus.types.Variant; import de.swiesend.secretservice.Secret; import de.swiesend.secretservice.Service; import de.swiesend.secretservice.Static; import de.swiesend.secretservice.handlers.Messaging; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.ObjectPath; +import org.freedesktop.dbus.types.Variant; import java.util.Map; import java.util.Optional; @@ -24,9 +24,7 @@ public InternalUnsupportedGuiltRiddenInterface(Service service) { @Override public boolean changeWithMasterPassword(DBusPath collection, Secret original, Secret master) { return send("ChangeWithMasterPassword", "o(oayays)(oayays)", collection, original, master) - // TODO: check which condition should apply - // .map(Static.Utils::isNullOrEmpty) - .map(response -> !Static.Utils.isNullOrEmpty(response)) + .map(response -> (boolean) response[0]) // expect first parameter to be true when no error is present .orElse(false); } @@ -45,9 +43,7 @@ public Optional createWithMasterPassword(Map proper @Override public boolean unlockWithMasterPassword(DBusPath collection, Secret master) { return send("UnlockWithMasterPassword", "o(oayays)", collection, master) - // TODO: check which condition should apply - // .map(Static.Utils::isNullOrEmpty) - .map(response -> !Static.Utils.isNullOrEmpty(response)) + .map(response -> (boolean) response[0]) // expect first parameter to be true when no error is present .orElse(false); } diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index fe26e35..2ee2801 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -97,8 +97,13 @@ public Optional send(String service, String path, String iface, String log.error("Unexpected org.freedesktop.dbus.errors.Error: \"" + error + "\" with parameters: " + Arrays.deepToString(parameters)); return Optional.empty(); } + } else { + if (parameters != null && parameters.length == 0) + return Optional.of(new Object[]{true}); // indicate with a boolean that there was no error + else { + return Optional.ofNullable(parameters); + } } - return Optional.ofNullable(parameters); } catch (DBusException e) { log.error("Unexpected D-Bus response: ", e); } catch (RuntimeException e) { diff --git a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java index add7ab1..95138fa 100644 --- a/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java +++ b/src/test/java/de/swiesend/secretservice/integration/keyring/InternalUnsupportedGuiltRiddenInterfaceTest.java @@ -11,6 +11,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.HashMap; @@ -21,6 +23,8 @@ public class InternalUnsupportedGuiltRiddenInterfaceTest { + private static final Logger log = LoggerFactory.getLogger(InternalUnsupportedGuiltRiddenInterfaceTest.class); + private DBusConnection connection; private Service service; private InternalUnsupportedGuiltRiddenInterface iugri; @@ -65,9 +69,9 @@ public void changeWithMasterPassword() throws InterruptedException { lock.add(collection.getPath()); service.lock(lock); - assertDoesNotThrow(() -> iugri.unlockWithMasterPassword(collection.getPath(), master)); - - iugri.changeWithMasterPassword(collection.getPath(), master, original); + // TODO: fix occasional: org.gnome.keyring.Error.Denied: "The password was invalid" + assertTrue(iugri.unlockWithMasterPassword(collection.getPath(), master)); + assertTrue(iugri.changeWithMasterPassword(collection.getPath(), master, original)); } @Test From 5eecd9e76d7b661b2e0c799e58708938ab0c0ead Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:27:31 +0200 Subject: [PATCH 40/74] Remove unnecessary exception imports --- .../de/swiesend/secretservice/functional/Collection.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 5ac368f..ae29294 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -12,12 +12,6 @@ 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.time.Duration; import java.util.*; From d0834e929ec2016c7c59e06fd05cddee07d2b96f Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:30:40 +0200 Subject: [PATCH 41/74] Remove solved TODOs --- .../java/de/swiesend/secretservice/TransportEncryption.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index 810e459..8718543 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -174,7 +174,6 @@ public Session getSession() { return session; } - // TODO: fix interface usage public Optional encrypt(CharSequence plain) { final byte[] bytes = Secret.toBytes(plain); Optional secret = encrypt(bytes, StandardCharsets.UTF_8); @@ -183,7 +182,6 @@ public Optional encrypt(CharSequence plain) { return secret; } - // TODO: fix interface usage public Optional encrypt(byte[] plain, Charset charset) { if (Static.Utils.isNullOrEmpty(plain)) return Optional.empty(); @@ -223,7 +221,6 @@ public Optional encrypt(byte[] plain, Charset charset) { return Optional.empty(); } - // TODO: return Optional; remove exceptions; public Optional decrypt(Secret secret) { if (secret == null) return Optional.empty(); From 1ebe2e972de195873d6fe7c48b1caf57afa33397 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:31:57 +0200 Subject: [PATCH 42/74] Alter comment --- .../java/de/swiesend/secretservice/TransportEncryption.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/TransportEncryption.java b/src/main/java/de/swiesend/secretservice/TransportEncryption.java index 8718543..450ca1e 100644 --- a/src/main/java/de/swiesend/secretservice/TransportEncryption.java +++ b/src/main/java/de/swiesend/secretservice/TransportEncryption.java @@ -228,7 +228,8 @@ public Optional decrypt(Secret secret) { if (sessionKey == null) { log.error("Missing session key. Call Opened.generateSessionKey() first."); } - byte[] decrypted = new byte[0]; // TODO: should this be final? How to handle the final Secret.clear? + // TODO: should decrypted be a final value? How to handle the finally Secret.clear? + byte[] decrypted = new byte[0]; try { IvParameterSpec ivSpec = new IvParameterSpec(secret.getSecretParameters()); Cipher cipher = Cipher.getInstance(Static.Algorithm.AES_CBC_PKCS5); From 8b2c7399e6cb133352ff05aebdbfb4cb01a1fe25 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:38:45 +0200 Subject: [PATCH 43/74] Move end-to-end example --- .../functional/SecretServiceTest.java | 76 --------------- .../functional/integration/Example.java | 93 +++++++++++++++++++ 2 files changed, 93 insertions(+), 76 deletions(-) create mode 100644 src/test/java/de/swiesend/secretservice/functional/integration/Example.java diff --git a/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java index ca2431a..2023d88 100644 --- a/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java @@ -34,82 +34,6 @@ void afterEach() throws Exception { secretService.close(); } - @Test - @DisplayName("Fiddling around") - public void fiddlingAround() throws Exception { - - char[] password; - try (ServiceInterface service = SecretService.create().get()) { - CollectionInterface collection = service - .openSession() - //.flatMap(session -> session.collection("täst", Optional.of("test"))) - .flatMap(session -> session.collection("täst", Optional.empty())) - //.flatMap(session -> session.defaultCollection()) - .get(); - String collectionLabel = collection.getLabel().get(); - String collectionId = collection.getId().get(); - log.info(String.format("Collection {label: %s, id: %s}", collectionLabel, collectionId)); - - Map attributes = Map.of("key", "value"); - String item = collection.createItem("label", "password", attributes).get(); - password = collection.getSecret(item).get(); - - for (String itemPath : collection.getItems(attributes).get()) { - String label = collection.getItemLabel(itemPath).get(); - log.info(String.format("[%s] {label: %s, attributes: %s}", itemPath, label, attributes)); - collection.deleteItem(itemPath); - } - // WARN: be careful activating this on the default collection... - if (collectionId != "default") { - log.info("Non default collection. Deleting collection..."); - // collection.delete(); - } - - // CollectionInterface col2 = service.openSession() - // .flatMap(session -> session.collection("test", "test")) - // .get(); - } - assertEquals("password", new String(password)); - password = "".toCharArray(); - - /*try (ServiceInterface service = SecretService.create().get()) { - try (SessionInterface session = service.openSession().get()) { - try (CollectionInterface collection = session.collection("test", "test").get()) { - String item = collection.createItem("label", "password", Map.of("key", "value")).get(); - Optional secret = collection.getSecret(item); - collection.delete(); - password = secret.get(); - } - } - } - assertEquals("password", new String(password)); - password = "".toCharArray(); - - password = SecretService.create().flatMap( - service -> service.openSession()).flatMap( - session -> session.collection("test", "test")).flatMap( - collection -> { - String item = collection.createItem("label", "password", Map.of("key", "value")).get(); - Optional secret = collection.getSecret(item); - collection.delete(); - return secret; - }).get(); - - assertEquals("password", new String(password));*/ - - - /*try (ServiceInterface service = SecretService.create().get()){ - try (SessionInterface session = service.openSession().get()) { - try (CollectionInterface collection = session.collection("test", "test").get()) { - String item = collection.createItem("label", "password", Map.of("key", "value")).get(); - Optional secret = collection.getSecret(item); - collection.delete(); - secret; - } - } - }*/ - } - @Test void create() { assertNotNull(secretService); diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java new file mode 100644 index 0000000..4e85555 --- /dev/null +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -0,0 +1,93 @@ +package de.swiesend.secretservice.functional.integration; + +import de.swiesend.secretservice.functional.SecretService; +import de.swiesend.secretservice.functional.interfaces.CollectionInterface; +import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class Example { + private static final Logger log = LoggerFactory.getLogger(Example.class); + + @Test + @DisplayName("API 2.0 Example") + public void endToEndTest() throws Exception { + + char[] password; + try (ServiceInterface service = SecretService.create().get()) { + CollectionInterface collection = service + .openSession() + //.flatMap(session -> session.collection("täst", Optional.of("test"))) + .flatMap(session -> session.collection("täst", Optional.empty())) + //.flatMap(session -> session.defaultCollection()) + .get(); + String collectionLabel = collection.getLabel().get(); + String collectionId = collection.getId().get(); + log.info(String.format("Collection {label: %s, id: %s}", collectionLabel, collectionId)); + + Map attributes = Map.of("key", "value"); + String item = collection.createItem("label", "password", attributes).get(); + password = collection.getSecret(item).get(); + + for (String itemPath : collection.getItems(attributes).get()) { + String label = collection.getItemLabel(itemPath).get(); + log.info(String.format("[%s] {label: %s, attributes: %s}", itemPath, label, attributes)); + collection.deleteItem(itemPath); + } + // WARN: be careful activating this on the default collection... + if (collectionId != "default") { + log.info("Non default collection. Deleting collection..."); + // collection.delete(); + } + + // CollectionInterface col2 = service.openSession() + // .flatMap(session -> session.collection("test", "test")) + // .get(); + } + assertEquals("password", new String(password)); + + /*try (ServiceInterface service = SecretService.create().get()) { + try (SessionInterface session = service.openSession().get()) { + try (CollectionInterface collection = session.collection("test", "test").get()) { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + password = secret.get(); + } + } + } + assertEquals("password", new String(password)); + password = "".toCharArray(); + + password = SecretService.create().flatMap( + service -> service.openSession()).flatMap( + session -> session.collection("test", "test")).flatMap( + collection -> { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + return secret; + }).get(); + + assertEquals("password", new String(password));*/ + + + /*try (ServiceInterface service = SecretService.create().get()){ + try (SessionInterface session = service.openSession().get()) { + try (CollectionInterface collection = session.collection("test", "test").get()) { + String item = collection.createItem("label", "password", Map.of("key", "value")).get(); + Optional secret = collection.getSecret(item); + collection.delete(); + secret; + } + } + }*/ + } +} From db5e6e72d601e944fe06b9c3240f9f497538e419 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:42:45 +0200 Subject: [PATCH 44/74] Activate collection.delete() for the example --- .../java/de/swiesend/secretservice/functional/Collection.java | 2 +- .../swiesend/secretservice/functional/integration/Example.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index ae29294..9b445c2 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -235,7 +235,7 @@ public boolean delete() { ObjectPath promptPath = collection.delete().get(); performPrompt(promptPath); } else { - log.error("Default collections may not be deleted with the simple API."); + log.error("Default collections shall only be deleted with the low-level API."); return false; } return true; diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java index 4e85555..17510cb 100644 --- a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -44,7 +44,7 @@ public void endToEndTest() throws Exception { // WARN: be careful activating this on the default collection... if (collectionId != "default") { log.info("Non default collection. Deleting collection..."); - // collection.delete(); + collection.delete(); } // CollectionInterface col2 = service.openSession() From f89d0059606faaf8d9c36fced94f457a582474f6 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Thu, 27 Jul 2023 22:45:48 +0200 Subject: [PATCH 45/74] Change doc-string --- .../de/swiesend/secretservice/functional/SecretService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/SecretService.java b/src/main/java/de/swiesend/secretservice/functional/SecretService.java index 39ff839..fad549c 100644 --- a/src/main/java/de/swiesend/secretservice/functional/SecretService.java +++ b/src/main/java/de/swiesend/secretservice/functional/SecretService.java @@ -12,7 +12,7 @@ import static de.swiesend.secretservice.Static.DEFAULT_PROMPT_TIMEOUT; /** - * Entrypoint for high-level API. Manages Secret-Service sessions and the D-Bus connection. + * Entrypoint for the functional high-level API. Manages Secret-Service sessions and the D-Bus connection. */ public class SecretService extends ServiceInterface { From cf292a69940a5bcec779bcd9c5681c268d7755c7 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Fri, 28 Jul 2023 15:42:29 +0200 Subject: [PATCH 46/74] Adapt performPrompt() and unlock() returning boolean --- .../secretservice/functional/Collection.java | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 9b445c2..0d8bab7 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -8,6 +8,7 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.types.Variant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,7 +89,13 @@ private Optional createNewCollection(String label) { Map properties = de.swiesend.secretservice.Collection.createProperties(label); if (encryptedCollectionPassword.isEmpty()) { - path = createCollectionWithPrompt(properties); + Optional maybePath = createCollectionWithPrompt(properties); + if (maybePath.isPresent()) { + path = maybePath.get(); + log.trace("Created collection at path: " + path); // TODO: make log.info + } else { + return Optional.empty(); + } } else if (service.isOrgGnomeKeyringAvailable()) { path = withoutPrompt.createWithMasterPassword(properties, encryptedCollectionPassword.get()).get(); } @@ -96,17 +103,21 @@ private Optional createNewCollection(String label) { if (path == null) { waitForCollectionCreatedSignal(); Service.CollectionCreated signal = service.getService().getSignalHandler().getLastHandledSignal(Service.CollectionCreated.class); + if (signal == null) { + log.warn("Collection \"" + label + "\" was not created."); + return Optional.empty(); + } + DBusPath signalPath = signal.collection; if (signalPath == null || signalPath.getPath() == null) { log.error(String.format("Received bad signal `CollectionCreated` without proper collection path: %s", signal)); - return null; + return Optional.empty(); } path = Static.Convert.toObjectPath(signalPath.getPath()); } if (path == null) { log.error("Could not acquire a path for the prompt."); - return null; } return Optional.ofNullable(path); @@ -120,13 +131,13 @@ private void waitForCollectionCreatedSignal() { } } - private ObjectPath createCollectionWithPrompt(Map properties) { + private Optional createCollectionWithPrompt(Map properties) { Pair response = service.getService().createCollection(properties).get(); if (!"/".equals(response.a.getPath())) { - return response.a; + return Optional.of(response.a); + } else { + return performPrompt(response.b); } - performPrompt(response.b); - return null; } private Optional getCollectionFromPath(ObjectPath path, String label) { @@ -164,13 +175,16 @@ private boolean isDefault() { } } - private void performPrompt(ObjectPath path) { + private Optional performPrompt(ObjectPath path) { if (!("/".equals(path.getPath()))) { - prompt.await(path, timeout); + return Optional.of(prompt.await(path, timeout)) + .filter(completed -> !completed.dismissed) + .map(success -> new ObjectPath(success.getSource(), success.result.getValue().toString())); + } else { + return Optional.empty(); } } - @Override public boolean clear() { if (encryptedCollectionPassword.isPresent()) { @@ -233,12 +247,11 @@ public Optional createItem(String label, CharSequence password, Map, ObjectPath> response = service.getService().unlock(lockable()).get(); - performPrompt(response.b); - if (!collection.isLocked()) { - isUnlockedOnceWithUserPermission = true; - log.info("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + Optional, ObjectPath>> maybeResponse = service.getService().unlock(lockable()); + if (maybeResponse.isPresent()) { + ObjectPath promptPath = maybeResponse.get().b; + if (performPrompt(promptPath).isPresent() && !collection.isLocked()) { + isUnlockedOnceWithUserPermission = true; + log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + return true; + } } } else if (encryptedCollectionPassword.isPresent() && service.isOrgGnomeKeyringAvailable()) { - withoutPrompt.unlockWithMasterPassword(collection.getPath(), encryptedCollectionPassword.get()); - log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + boolean result = withoutPrompt.unlockWithMasterPassword(collection.getPath(), encryptedCollectionPassword.get()); + if (result == true) { + log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + } + return result; } } + log.debug("Could not unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); + return false; } @Override From 6272921aeba4481d52cf93d9ae76471ae09bb613 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Fri, 28 Jul 2023 17:02:31 +0200 Subject: [PATCH 47/74] Allow fine-grained auto-close for the system creating the service with create() assumes to create a system and manage the auto-close by the service. --- .../secretservice/functional/SecretService.java | 12 +++++++++++- .../de/swiesend/secretservice/functional/System.java | 2 +- .../functional/interfaces/AvailableServices.java | 2 +- .../functional/interfaces/ServiceInterface.java | 2 +- .../functional/interfaces/SystemInterface.java | 2 +- .../secretservice/functional/SecretServiceTest.java | 7 ++----- .../secretservice/functional/SystemTest.java | 3 ++- 7 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/SecretService.java b/src/main/java/de/swiesend/secretservice/functional/SecretService.java index fad549c..bd10b88 100644 --- a/src/main/java/de/swiesend/secretservice/functional/SecretService.java +++ b/src/main/java/de/swiesend/secretservice/functional/SecretService.java @@ -20,6 +20,8 @@ public class SecretService extends ServiceInterface { private Map sessions = new HashMap<>(); private de.swiesend.secretservice.Service service; + private static Optional maybeSystem = Optional.empty(); + private boolean gnomeKeyringAvailable; private Duration timeout = DEFAULT_PROMPT_TIMEOUT; @@ -33,7 +35,12 @@ private SecretService(SystemInterface system, AvailableServices available) { * Create a Secret-Service instance with initialized transport encryption. */ public static Optional create() { - return System.connect() + maybeSystem = System.connect(); + return create(maybeSystem); + } + + public static Optional create(Optional maybeSystem) { + return maybeSystem .map(system -> new Pair<>(system, new AvailableServices(system))) .filter(pair -> isAvailable(pair.a, pair.b)) .map(pair -> new SecretService(pair.a, pair.b)); @@ -87,6 +94,9 @@ public void close() throws Exception { session.close(); } } + if (maybeSystem.isPresent()) { + maybeSystem.get().close(); + } } public de.swiesend.secretservice.Service getService() { diff --git a/src/main/java/de/swiesend/secretservice/functional/System.java b/src/main/java/de/swiesend/secretservice/functional/System.java index 7c2fb1d..f1743b8 100644 --- a/src/main/java/de/swiesend/secretservice/functional/System.java +++ b/src/main/java/de/swiesend/secretservice/functional/System.java @@ -27,7 +27,7 @@ private System(DBusConnection connection) { * * @return a new `DBusConnection` or `Optional.empty()` */ - public static Optional connect() { + public static Optional connect() { try { DBusConnection dbus = DBusConnectionBuilder.forSessionBus().build(); return Optional.of(new System(dbus)); diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java index 028cce2..d470081 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/AvailableServices.java @@ -18,7 +18,7 @@ public class AvailableServices { public EnumSet services = EnumSet.noneOf(Activatable.class); - public AvailableServices(System system) { + public AvailableServices(SystemInterface system) { DBusConnection connection = system.getConnection(); if (connection.isConnected()) { try { diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java index b7147d2..1c27cf6 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java @@ -47,7 +47,7 @@ private static Optional setupShutdownHook() { * * @return true if the secret service is available, otherwise false and will log an error message. */ - public static boolean isAvailable(System system, AvailableServices available) { + public static boolean isAvailable(SystemInterface system, AvailableServices available) { DBusConnection connection = system.getConnection(); if (connection.isConnected()) { try { diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java index b95d577..63bb012 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/SystemInterface.java @@ -11,7 +11,7 @@ public abstract class SystemInterface implements AutoCloseable { private static final Logger log = LoggerFactory.getLogger(SystemInterface.class); - public static Optional connect() { + public static Optional connect() { log.warn("Do not call the interface method, but the implementation."); return null; } diff --git a/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java index 2023d88..5a500cc 100644 --- a/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SecretServiceTest.java @@ -1,9 +1,6 @@ package de.swiesend.secretservice.functional; -import de.swiesend.secretservice.functional.interfaces.AvailableServices; -import de.swiesend.secretservice.functional.interfaces.CollectionInterface; -import de.swiesend.secretservice.functional.interfaces.ServiceInterface; -import de.swiesend.secretservice.functional.interfaces.SessionInterface; +import de.swiesend.secretservice.functional.interfaces.*; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -41,7 +38,7 @@ void create() { @Test void isOrgGnomeKeyringAvailable() { - System system = System.connect().get(); + SystemInterface system = System.connect().get(); assertTrue(SecretService.isAvailable(system, new AvailableServices(system))); } diff --git a/src/test/java/de/swiesend/secretservice/functional/SystemTest.java b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java index 3651238..39b722d 100644 --- a/src/test/java/de/swiesend/secretservice/functional/SystemTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/SystemTest.java @@ -1,5 +1,6 @@ package de.swiesend.secretservice.functional; +import de.swiesend.secretservice.functional.interfaces.SystemInterface; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -10,7 +11,7 @@ class SystemTest { - System system; + SystemInterface system; @BeforeEach void beforeEach() { From dc1453a8cc60dfaf11e9d5824d3e1a4cfd0e2bf2 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Fri, 28 Jul 2023 17:10:47 +0200 Subject: [PATCH 48/74] Fix collection.delete() and add basic tests --- .../secretservice/functional/Collection.java | 11 +- .../functional/CollectionTest.java | 213 ++++++++++++++++++ 2 files changed, 219 insertions(+), 5 deletions(-) create mode 100644 src/test/java/de/swiesend/secretservice/functional/CollectionTest.java diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 0d8bab7..818e227 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -8,7 +8,6 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.connections.impl.DBusConnection; -import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.types.Variant; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,6 +53,7 @@ public Collection(SessionInterface session, String label, Optional this.encryptedCollectionPassword = maybePassword .flatMap(password -> session.getEncryptedSession().encrypt(password)); + // TODO: the constructor may not throw an error... this.collection = getOrCreateCollection(label) .orElseThrow(() -> new NoSuchElementException(String.format("Cloud not acquire collection with name %s", label))); this.path = collection.getPath(); @@ -245,13 +245,14 @@ public Optional createItem(String label, CharSequence password, Map promptPath.getPath().equals("/") || performPrompt(promptPath).isPresent()) + .orElse(false); } @Override diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java new file mode 100644 index 0000000..89d1502 --- /dev/null +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -0,0 +1,213 @@ +package de.swiesend.secretservice.functional; + +import de.swiesend.secretservice.functional.interfaces.CollectionInterface; +import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CollectionTest { + + private static final Logger log = LoggerFactory.getLogger(CollectionTest.class); + + ServiceInterface service = null; + CollectionInterface collection = null; + + @BeforeEach + void setUp() { + service = SecretService.create().get(); + collection = service + .openSession() + .flatMap(session -> session.collection("test-collection", Optional.of("collection"))) + .get(); + } + + @AfterEach + void tearDown() throws Exception { + collection.delete(); + collection.close(); + service.close(); + } + + @Test + @Disabled + // TODO + void clear() { + } + + @Test + void createItem() { + for (String i : Arrays.asList("a", "b", "c")) { + String item = collection.createItem(i, i).get(); + assertTrue(item.startsWith("/org/freedesktop/secrets/collection/test_2dcollection/")); + String label = collection.getItemLabel(item).get(); + assertEquals(i, label); + char[] secret = collection.getSecret(item).get(); + assertEquals(i, new String(secret)); + } + } + + @Test + void createItemWithAttribute() { + for (String i : Arrays.asList("a", "b", "c")) { + Map expectedAttributes = Map.of(i, i, i + i, i + i); + String item = collection.createItem(i, i, expectedAttributes).get(); + assertTrue(item.startsWith("/org/freedesktop/secrets/collection/test_2dcollection/")); + + String label = collection.getItemLabel(item).get(); + assertEquals(i, label); + + char[] secret = collection.getSecret(item).get(); + assertEquals(i, new String(secret)); + + Map actualAttributes = collection.getAttributes(item).get(); + for (Map.Entry entry : actualAttributes.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + log.info(key + ": " + value); + } + assertEquals(expectedAttributes.size(), actualAttributes.size()); + for (Map.Entry entry : expectedAttributes.entrySet()) { + String key = entry.getKey(); + String expectedValue = entry.getValue(); + String actualValue = actualAttributes.get(key); + assertEquals(expectedValue, actualValue); + } + } + } + + @Test + void delete() { + assertTrue(collection.delete()); + } + + @Test + void deleteALockedCollection() { + assertTrue(collection.lock()); + assertTrue(collection.delete()); + } + + @Test + @Disabled + void deleteWithALockedService() { + assertTrue(service.getService().lockService()); + assertTrue(collection.delete()); + } + + @Test + @Disabled + // TODO: test password abort + void deleteCollectionWithoutPassword() { + CollectionInterface collectionWithoutPassword = service + .openSession() + .flatMap(session -> session.collection("test-no-password-collection", Optional.empty())) + .get(); + assertTrue(collectionWithoutPassword.delete()); + } + + @Test + @Disabled + // TODO + void deleteItem() { + } + + @Test + @Disabled + // TODO + void deleteItems() { + } + + @Test + @Disabled + // TODO + void getAttributes() { + } + + @Test + @Disabled + // TODO + void getItems() { + } + + @Test + @Disabled + // TODO + void getItemLabel() { + } + + @Test + @Disabled + // TODO + void setItemLabel() { + } + + @Test + @Disabled + // TODO + void setLabel() { + } + + @Test + @Disabled + // TODO + void getLabel() { + } + + @Test + @Disabled + // TODO + void getId() { + } + + @Test + @Disabled + // TODO + void getSecret() { + } + + @Test + @Disabled + // TODO + void getSecrets() { + } + + @Test + @Disabled + // TODO + void isLocked() { + } + + @Test + @Disabled + // TODO + void lock() { + } + + @Test + @Disabled + // TODO + void unlockWithUserPermission() { + } + + @Test + @Disabled + // TODO + void updateItem() { + } + + @Test + @Disabled + // TODO + void close() { + } +} \ No newline at end of file From f60dc35ed75834817766c89f9c64e8bf9d610f8a Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 2 Oct 2023 22:05:22 +0200 Subject: [PATCH 49/74] Fix typo --- .../java/de/swiesend/secretservice/functional/Collection.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 818e227..49b7083 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -55,7 +55,9 @@ public Collection(SessionInterface session, String label, Optional // TODO: the constructor may not throw an error... this.collection = getOrCreateCollection(label) - .orElseThrow(() -> new NoSuchElementException(String.format("Cloud not acquire collection with name %s", label))); + .orElseThrow(() -> new NoSuchElementException( + String.format("Could not acquire collection with name %s", label) + )); this.path = collection.getPath(); this.label = label; this.id = collection.getId(); From cc6cf6e3934ce815273ede7045f93094c77faa1a Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 2 Oct 2023 22:40:13 +0200 Subject: [PATCH 50/74] Simplify path extraction --- .../de/swiesend/secretservice/Prompt.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/Prompt.java b/src/main/java/de/swiesend/secretservice/Prompt.java index da79f38..f6c3c22 100644 --- a/src/main/java/de/swiesend/secretservice/Prompt.java +++ b/src/main/java/de/swiesend/secretservice/Prompt.java @@ -1,14 +1,14 @@ package de.swiesend.secretservice; +import de.swiesend.secretservice.handlers.Messaging; import org.freedesktop.dbus.ObjectPath; import org.freedesktop.dbus.messages.DBusSignal; -import de.swiesend.secretservice.errors.NoSuchObject; -import de.swiesend.secretservice.handlers.Messaging; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.Arrays; import java.util.List; -import java.util.Optional; import static de.swiesend.secretservice.Static.DEFAULT_PROMPT_TIMEOUT; import static de.swiesend.secretservice.Static.ObjectPaths.PROMPT; @@ -17,6 +17,8 @@ public class Prompt extends Messaging implements de.swiesend.secretservice.inter public static final List> signals = Arrays.asList(Completed.class); + private static final Logger log = LoggerFactory.getLogger(Prompt.class); + public Prompt(Service service) { super(service.getConnection(), signals, Static.Service.SECRETS, @@ -36,14 +38,12 @@ public boolean prompt(ObjectPath prompt) { String windowID = ""; - try { - if (objectPath.startsWith(PROMPT + "/p") || objectPath.startsWith(PROMPT + "/u")) { - String[] split = prompt.getPath().split("/"); - windowID = split[split.length - 1]; + if (objectPath.startsWith(PROMPT + "/p") || objectPath.startsWith(PROMPT + "/u")) { + try { + windowID = objectPath.substring(objectPath.lastIndexOf("/") + 1); + } catch (IndexOutOfBoundsException | NullPointerException e) { + log.warn(String.format("Continuing with window ID: \"%s\"", windowID)); } - } catch (IndexOutOfBoundsException | NullPointerException e) { - // TODO: check if it is possible to can call the prompt anyway - return false; } return send("Prompt", "s", windowID).isPresent(); From a3214b68759d690c2dd504f02d6c4b3f3e9af379 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Mon, 2 Oct 2023 22:41:19 +0200 Subject: [PATCH 51/74] Add and --- pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pom.xml b/pom.xml index 15247fc..e2d0b95 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,8 @@ UTF-8 17 + 17 + 17 17 From 7a07339ffce08172e57a664e8aa0586904b8ca61 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 14:59:39 +0200 Subject: [PATCH 52/74] Elaborate mitigation on CVE-2018-19358 --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa1bc6a..24ef660 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Maven Central](https://img.shields.io/maven-central/v/de.swiesend/secret-service.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22de.swiesend%22%20AND%20a:%22secret-service%22) -A _Java_ library for storing secrets in the _Gnome Keyring_ over _DBus_. +A _Java_ library for storing secrets in a keyring over the _DBus_. The library is conform to the freedesktop.org [Secret Service API 0.2](https://specifications.freedesktop.org/secret-service/0.2) and thus compatible with Gnome linux systems. @@ -19,9 +19,49 @@ For KDE systems there is the [`kdewallet`](https://github.com/purejava/kdewallet ### CVE-2018-19358 (Vulnerability) -There is a current investigation on the behaviour of the Secret Service API, as other applications can easily read __any__ secret, if the keyring is unlocked (if a user is logged in, then the `login`/`default` collection is unlocked). Available D-Bus protection mechanisms (involving the busconfig and policy XML elements) are not used by default. The Secret Service API was never designed with a secure retrieval mechanism. - -* [CVE-2018-19358](https://nvd.nist.gov/vuln/detail/CVE-2018-19358) Base Score: __[7.8 HIGH]__, CVSS:3.0 +There is an investigation on the behaviour of the Secret Service API, as other applications can easily read __any__ secret, if the keyring is unlocked (if a user is logged in, then the `login`/`default` collection is unlocked). +Available D-Bus protection mechanisms (involving the `busconfig` and `policy XML elements) are not used by default. But D-Bus protection mechanisms are not sufficient to protect against malicious attackers, because applications could identify themselves as different applications with various mechanisms. +The Secret Service API was never designed with a secure retrieval mechanism, as this problem is mainly a design problem in the Linux desktop itself, which does not provide _Sandboxing_ (like Flatpak, sandbox, containers) for applications by default. + +The attack vector is known, see GnomeKeyring [SecurityFAQ](https://wiki.gnome.org/Projects/GnomeKeyring/SecurityFAQ), [SecurityPhilosophy](https://wiki.gnome.org/Projects/GnomeKeyring/SecurityPhilosophy) and [disputed](https://gitlab.gnome.org/GNOME/gnome-keyring/-/issues/5) because the behavior represents a design decision. + +| Publisher | Url | Base Score | Vector | Published | Last Update | Status | +|-----------|-------------------------------------------------------------------------------------------------------------------------------------------------|------------------|--------|-----------|-------------|-------------| +| NVD NIST | [CVE-2018-19358](https://nvd.nist.gov/vuln/detail/CVE-2018-19358) | __[7.8 HIGH]__ | CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H | 2018-11-18 | 2020-08-24 | active | +| Cisco | [GNOME Keyring Secret Service API Login Credentials Retrieval Vulnerability](https://tools.cisco.com/security/center/viewAlert.x?alertId=59179) | __[5.5 MEDIUM]__ | CVSS:3.0 | | | unpublished | +| Red Hat | [CVE-2018-19358](https://access.redhat.com/security/cve/cve-2018-19358) | __[4.3 MEDIUM]__ | CVSS:3.0/AV:P/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N | 2018-07-06 | 2023-04-06 | Red Hat Product Security determined that this flaw was not a security vulnerability, but a design problem in the Linux desktop | +| Suse | related issue [CVE-2008-7320](https://www.suse.com/security/cve/CVE-2008-7320.html) | [2.1 LOW] | CVSS:2.0/AV:L/AC:L/Au:N/C:P/I:N/A:N | 2018-11-09 | 2023-07-03 | Resolved | + +**Mitigation** + +**Not recommended** +- Storing secrets in the `login`/`default` keyring, when there are potentially malicious applications installed by the user. This is often the case in not well maintained desktop environment. +- Implementing a `busconfig` for the D-Bus that enforces restrictions on the Secret Service API of the host system, knowing that these can be mitigated by providing a false sender/window/process id or dbus address. + +**Recommended** +- [`easy`] Storing secrets in a non-default collection that is always locked. This is a compromise that is useful when the user is willing to be prompted for the collection password, when accessing a secret. One can lock the collection after retrieval, so that the secrets are only exposed for a brief moment. +- [`easy`] Using [KeePassXC](https://keepassxc.org/) as provider. The KeePassXC implementation of the Secret Service API mitigates unauthorized retrievals by providing several access control mechanisms. + - [`easy`] Always locked collection: One can lock the collection after retrieval, so that the secrets are only exposed for a brief moment. +- [`easy`] Storing secrets in a file with proper permissions instead of using the Secret Service API. + - [`moderate`] There are projects like [SOPS](https://github.com/getsops/sops) for secret-management to encrypt and edit files. But again oneself has to store the encryption keys securely. + - [`easy`/`moderate`] Using disk encryption like [LUKS](https://gitlab.com/cryptsetup/cryptsetup) does not help against malicious applications, but at least against several scenarios with physical access. +- [`moderate`/`advanced`] Deliver your application in a secure sandbox. + +**KeePassXC** + +Notification: + - Show notification when passwords are retrieved by clients + +Access Control: + - Confirm when passwords are retrieved by clients. + - Confirm when clients request entry deletion. + - Prompt to unlock database before searching. + - Management of an exposed database group, instead of the whole database. + - Prohibiting the deletion of the database. Only entries can be deleted, but are moved to the "Recycle Bin" group by default. + +Authorization: + - Showing connected applications by PID and DBus Address. + - Using a keyfile that has to be present when accessing the collection. ## Usage From f625a1235ca29d46d085dfd1583ea92aa445e56e Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:04:09 +0200 Subject: [PATCH 53/74] Fix prompting for KeePassXC --- src/main/java/de/swiesend/secretservice/Prompt.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/Prompt.java b/src/main/java/de/swiesend/secretservice/Prompt.java index f6c3c22..849d0d9 100644 --- a/src/main/java/de/swiesend/secretservice/Prompt.java +++ b/src/main/java/de/swiesend/secretservice/Prompt.java @@ -38,12 +38,16 @@ public boolean prompt(ObjectPath prompt) { String windowID = ""; - if (objectPath.startsWith(PROMPT + "/p") || objectPath.startsWith(PROMPT + "/u")) { + if (objectPath.startsWith(PROMPT + "/")) { try { + // NOTE: If the windowID starts with the prefix 'p' or 'u' followed by numerics only, + // then it comes from the gnome-keyring. KeePassXC generates random alphanumeric ids. windowID = objectPath.substring(objectPath.lastIndexOf("/") + 1); } catch (IndexOutOfBoundsException | NullPointerException e) { - log.warn(String.format("Continuing with window ID: \"%s\"", windowID)); + log.warn(String.format("No proper window ID. Continuing with window ID: \"%s\"", windowID)); } + } else { + log.warn(String.format("No proper prompt path. Continuing with window ID: \"%s\"", windowID)); } return send("Prompt", "s", windowID).isPresent(); From e7ae714a3b606989de1a00352d5bdf8d82683f97 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:06:26 +0200 Subject: [PATCH 54/74] Simplify collection retrieval --- .../de/swiesend/secretservice/functional/Collection.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 49b7083..6137131 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -75,11 +75,9 @@ private void init(SessionInterface session) { } private Optional getOrCreateCollection(String label) { - Optional maybePath; + Optional maybePath = getCollectionPath(label); - if (exists(label)) { - maybePath = getCollectionPath(label); - } else { + if (maybePath.isEmpty()) { maybePath = createNewCollection(label); } From b30229568572c19bba702e922f92f68d9266bd2c Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:06:38 +0200 Subject: [PATCH 55/74] Remove logging for collection creation --- .../java/de/swiesend/secretservice/functional/Collection.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 6137131..b4079ca 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -92,7 +92,6 @@ private Optional createNewCollection(String label) { Optional maybePath = createCollectionWithPrompt(properties); if (maybePath.isPresent()) { path = maybePath.get(); - log.trace("Created collection at path: " + path); // TODO: make log.info } else { return Optional.empty(); } From 52b423a6d5f967c1cc053cb3f45c4c15d5e9dc32 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:07:02 +0200 Subject: [PATCH 56/74] Reformat nested statement --- .../swiesend/secretservice/functional/Collection.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index b4079ca..78c0b69 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -215,11 +215,14 @@ public Optional createItem(String label, CharSequence password, Map result = session.getEncryptedSession().encrypt(password) + Optional result = session + .getEncryptedSession() + .encrypt(password) .flatMap(secret -> { try (secret) { // auto-close final Map properties = Item.createProperties(label, attributes); - return collection.createItem(properties, secret, false) + return collection + .createItem(properties, secret, false) .flatMap(pair -> Optional.ofNullable(pair.a) .map(item -> { if ("/".equals(item.getPath())) { // prompt required @@ -236,7 +239,8 @@ public Optional createItem(String label, CharSequence password, Map Date: Tue, 3 Oct 2023 15:43:54 +0200 Subject: [PATCH 57/74] Add documentation to the createItem interface --- .../secretservice/functional/Collection.java | 18 ++++----- .../interfaces/CollectionInterface.java | 40 +++++++++++++++++-- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 78c0b69..95ce4e0 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -193,14 +193,14 @@ public boolean clear() { } @Override - public Optional createItem(String label, CharSequence password) { - return createItem(label, password, null); + public Optional createItem(String label, CharSequence secret) { + return createItem(label, secret, null); } @Override - public Optional createItem(String label, CharSequence password, Map attributes) { - if (Static.Utils.isNullOrEmpty(password)) { - log.error("The password may not be null or empty."); + public Optional createItem(String label, CharSequence secret, Map attributes) { + if (Static.Utils.isNullOrEmpty(secret)) { + log.error("The secret may not be null or empty."); return Optional.empty(); } if (label == null) { @@ -217,12 +217,12 @@ public Optional createItem(String label, CharSequence password, Map result = session .getEncryptedSession() - .encrypt(password) - .flatMap(secret -> { - try (secret) { // auto-close + .encrypt(secret) + .flatMap(secInst -> { + try (secInst) { // auto-close final Map properties = Item.createProperties(label, attributes); return collection - .createItem(properties, secret, false) + .createItem(properties, secInst, false) .flatMap(pair -> Optional.ofNullable(pair.a) .map(item -> { if ("/".equals(item.getPath())) { // prompt required diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java index 4818816..f12affc 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java @@ -8,9 +8,43 @@ public interface CollectionInterface extends AutoCloseable { boolean clear(); - Optional createItem(String label, CharSequence password); - - Optional createItem(String label, CharSequence password, Map attributes); + /** + * Create an item in the collection. + * + * @param label The label is a description. It is not a unique identifier. + * + * @param secret The secret to be stored. One can encrypt the secret in beforehand, + * to avoid unwanted access from malicious applications. + * + * @return item path, which is a unique reference generated by the provider + */ + Optional createItem(String label, CharSequence secret); + + /** + * Create an item in the collection. + * + * @param label The label is a description. It is not a unique identifier. + * + * @param secret The secret to be stored. One can encrypt the secret in beforehand, + * to avoid unwanted access from malicious applications. + * + * @param attributes Attributes are provided as a mapping of custom keys and values. + * Applications often identify their secrets using the key `application` + * and their application name as value and providing a unique identifier + * for a specific secret in order to find it by its attributes instead + * of the generated item path. + * + * e.g.: + * + * { + * "application": "APPLICATION_NAME", + * "uuid": "f773dd93-4869-4897-be9d-c53f5b43f904" + * } + * + * + * @return item path, which is a unique reference generated by the provider + */ + Optional createItem(String label, CharSequence secret, Map attributes); boolean delete(); From 85291d46a6dde1a0cff7fbfa734bd7f786de720b Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:50:42 +0200 Subject: [PATCH 58/74] Remove unnecessary calls for the simple e2e test. deleteItem() behaves opposite as expected! --- .../secretservice/functional/Collection.java | 3 +- .../functional/integration/Example.java | 33 ++++++++++++------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 95ce4e0..e858c23 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -269,7 +269,8 @@ public boolean deleteItem(String objectPath) { Item item = getItem(objectPath).get(); ObjectPath promptPath = item.delete().get(); - return performPrompt(promptPath).isPresent(); + // TODO: Fix this. Do not negate here, but return true if deleted. + return !performPrompt(promptPath).isPresent(); } @Override diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java index 17510cb..3c8aed5 100644 --- a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -12,6 +12,7 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public class Example { private static final Logger log = LoggerFactory.getLogger(Example.class); @@ -30,22 +31,30 @@ public void endToEndTest() throws Exception { .get(); String collectionLabel = collection.getLabel().get(); String collectionId = collection.getId().get(); - log.info(String.format("Collection {label: %s, id: %s}", collectionLabel, collectionId)); + log.info(String.format("Collection {label: \"%s\", id: \"%s\"}", collectionLabel, collectionId)); Map attributes = Map.of("key", "value"); - String item = collection.createItem("label", "password", attributes).get(); - password = collection.getSecret(item).get(); + String itemPath = collection.createItem("label", "password", attributes).get(); + log.info(String.format("Created Item {path: \"%s\"}", itemPath)); + password = collection.getSecret(itemPath).get(); + boolean deleteItemSuccess = collection.deleteItem(itemPath); + assertTrue(deleteItemSuccess); + + /*for (String oneItemPath : collection.getItems(attributes).get()) { + String label = collection.getItemLabel(oneItemPath).get(); + log.info(String.format("Delete Item {label: %s, attributes: %s, path: %s}", label, attributes, oneItemPath)); + collection.deleteItem(oneItemPath); + }*/ - for (String itemPath : collection.getItems(attributes).get()) { - String label = collection.getItemLabel(itemPath).get(); - log.info(String.format("[%s] {label: %s, attributes: %s}", itemPath, label, attributes)); - collection.deleteItem(itemPath); - } // WARN: be careful activating this on the default collection... - if (collectionId != "default") { - log.info("Non default collection. Deleting collection..."); - collection.delete(); - } + /*if (collectionLabel == "test" || collectionLabel == "täst" ) { + log.info(String.format("Deleting collection {label: \"%s\", id: \"%s\"} …", collectionLabel, collectionId)); + boolean success = collection.delete(); + if (success) + log.info(String.format("Deleted collection {label: \"%s\", id: \"%s\"}", collectionLabel, collectionId)); + else + log.warn(String.format("Could not delete collection {label: \"%s\", id: \"%s\"}", collectionLabel, collectionId)); + }*/ // CollectionInterface col2 = service.openSession() // .flatMap(session -> session.collection("test", "test")) From 9092419ed81e55c3884cc8bb79f7e52c9740fe65 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 3 Oct 2023 15:53:23 +0200 Subject: [PATCH 59/74] Update comment on keyring dependent results --- .../de/swiesend/secretservice/functional/Collection.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index e858c23..f5f80c5 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -269,8 +269,9 @@ public boolean deleteItem(String objectPath) { Item item = getItem(objectPath).get(); ObjectPath promptPath = item.delete().get(); - // TODO: Fix this. Do not negate here, but return true if deleted. - return !performPrompt(promptPath).isPresent(); + // TODO: Fix this. The gnome-keyring return false here, but KeePassXC return true. + // Handle depending on the keyring used. + return performPrompt(promptPath).isPresent(); } @Override From 82874879392009b3574a317a8ebf9d755a2859d8 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:01:24 +0200 Subject: [PATCH 60/74] Make label Optional and do not init Optionals with null --- .../secretservice/functional/Collection.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index f5f80c5..3f92e56 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -30,9 +30,9 @@ public class Collection implements CollectionInterface { DBusConnection connection = null; private Duration timeout = null; private Boolean isUnlockedOnceWithUserPermission = false; - private String label = null; + private Optional label = Optional.empty(); private String id = null; - private Optional encryptedCollectionPassword = null; + private Optional encryptedCollectionPassword = Optional.empty(); private Prompt prompt = null; private InternalUnsupportedGuiltRiddenInterface withoutPrompt = null; @@ -43,7 +43,7 @@ public Collection(SessionInterface session) { this.path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); this.collection = new de.swiesend.secretservice.Collection(path, connection); - this.label = collection.getLabel().get(); + this.label = collection.getLabel(); this.id = collection.getId(); } @@ -59,7 +59,7 @@ public Collection(SessionInterface session, String label, Optional String.format("Could not acquire collection with name %s", label) )); this.path = collection.getPath(); - this.label = label; + this.label = Optional.ofNullable(label); this.id = collection.getId(); } @@ -328,14 +328,14 @@ public boolean setItemLabel(String objectPath, String label) { public boolean setLabel(String label) { boolean success = collection.setLabel(label); if (success) { - this.label = label; + this.label = Optional.ofNullable(label); } return success; } @Override public Optional getLabel() { - return Optional.of(this.label); + return this.label; } @Override From 19ffa6fe36156f0a8e4aff9c1d3cda384a0359d5 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:08:13 +0200 Subject: [PATCH 61/74] Fix nullable Optional --- .../java/de/swiesend/secretservice/functional/Collection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 3f92e56..05bc550 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -176,7 +176,7 @@ private boolean isDefault() { private Optional performPrompt(ObjectPath path) { if (!("/".equals(path.getPath()))) { - return Optional.of(prompt.await(path, timeout)) + return Optional.ofNullable(prompt.await(path, timeout)) .filter(completed -> !completed.dismissed) .map(success -> new ObjectPath(success.getSource(), success.result.getValue().toString())); } else { From 857a02f0cea5cb6438ad6ff6aa53c5a9f98af018 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:13:06 +0200 Subject: [PATCH 62/74] Rename to isGnomeKeyringAvailable --- .../de/swiesend/secretservice/functional/Collection.java | 6 +++--- .../swiesend/secretservice/functional/SecretService.java | 8 ++++---- .../functional/interfaces/ServiceInterface.java | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 05bc550..dd9a8a0 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -69,7 +69,7 @@ private void init(SessionInterface session) { this.connection = service.getService().getConnection(); this.timeout = session.getService().getTimeout(); this.prompt = new Prompt(session.getService().getService()); - if (service.isOrgGnomeKeyringAvailable()) { + if (service.isGnomeKeyringAvailable()) { this.withoutPrompt = new InternalUnsupportedGuiltRiddenInterface(session.getService().getService()); } } @@ -95,7 +95,7 @@ private Optional createNewCollection(String label) { } else { return Optional.empty(); } - } else if (service.isOrgGnomeKeyringAvailable()) { + } else if (service.isGnomeKeyringAvailable()) { path = withoutPrompt.createWithMasterPassword(properties, encryptedCollectionPassword.get()).get(); } @@ -412,7 +412,7 @@ private boolean unlock() { return true; } } - } else if (encryptedCollectionPassword.isPresent() && service.isOrgGnomeKeyringAvailable()) { + } else if (encryptedCollectionPassword.isPresent() && service.isGnomeKeyringAvailable()) { boolean result = withoutPrompt.unlockWithMasterPassword(collection.getPath(), encryptedCollectionPassword.get()); if (result == true) { log.debug("Unlocked collection: \"" + collection.getLabel().get() + "\" (" + collection.getObjectPath() + ")"); diff --git a/src/main/java/de/swiesend/secretservice/functional/SecretService.java b/src/main/java/de/swiesend/secretservice/functional/SecretService.java index bd10b88..c764115 100644 --- a/src/main/java/de/swiesend/secretservice/functional/SecretService.java +++ b/src/main/java/de/swiesend/secretservice/functional/SecretService.java @@ -22,13 +22,13 @@ public class SecretService extends ServiceInterface { private static Optional maybeSystem = Optional.empty(); - private boolean gnomeKeyringAvailable; + private boolean isGnomeKeyringAvailable; private Duration timeout = DEFAULT_PROMPT_TIMEOUT; private SecretService(SystemInterface system, AvailableServices available) { this.service = new Service(system.getConnection()); - this.gnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); + this.isGnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); } /** @@ -47,8 +47,8 @@ public static Optional create(Optional maybeS } @Override - public boolean isOrgGnomeKeyringAvailable() { - return this.gnomeKeyringAvailable; + public boolean isGnomeKeyringAvailable() { + return this.isGnomeKeyringAvailable; } @Override diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java index 1c27cf6..80f1f2a 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/ServiceInterface.java @@ -1,7 +1,6 @@ package de.swiesend.secretservice.functional.interfaces; import de.swiesend.secretservice.TransportEncryption; -import de.swiesend.secretservice.functional.System; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,5 +95,5 @@ public static boolean isAvailable(SystemInterface system, AvailableServices avai abstract public de.swiesend.secretservice.Service getService(); - abstract public boolean isOrgGnomeKeyringAvailable(); + abstract public boolean isGnomeKeyringAvailable(); } From 651b6a57fee80c01c74b06852256c7308b9b8f73 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:17:55 +0200 Subject: [PATCH 63/74] Fix deleteItem() for gnome-keyring --- .../swiesend/secretservice/functional/Collection.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index dd9a8a0..a4766df 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -269,9 +269,14 @@ public boolean deleteItem(String objectPath) { Item item = getItem(objectPath).get(); ObjectPath promptPath = item.delete().get(); - // TODO: Fix this. The gnome-keyring return false here, but KeePassXC return true. - // Handle depending on the keyring used. - return performPrompt(promptPath).isPresent(); + Optional performedPrompt = performPrompt(promptPath); + if (service.isGnomeKeyringAvailable() && performedPrompt.isEmpty()) { + // gnome-keyring returns no path; + return true; + } else { + // KeePassXC returns an empty path + return performedPrompt.isPresent(); + } } @Override From e74f3e062958640737496f324ebe4002d89ac36d Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:39:59 +0200 Subject: [PATCH 64/74] Activate deleting test collection --- .../secretservice/functional/integration/Example.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java index 3c8aed5..b028cf4 100644 --- a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -47,14 +47,14 @@ public void endToEndTest() throws Exception { }*/ // WARN: be careful activating this on the default collection... - /*if (collectionLabel == "test" || collectionLabel == "täst" ) { + if (collectionLabel == "test" || collectionLabel == "täst" ) { log.info(String.format("Deleting collection {label: \"%s\", id: \"%s\"} …", collectionLabel, collectionId)); boolean success = collection.delete(); if (success) log.info(String.format("Deleted collection {label: \"%s\", id: \"%s\"}", collectionLabel, collectionId)); else log.warn(String.format("Could not delete collection {label: \"%s\", id: \"%s\"}", collectionLabel, collectionId)); - }*/ + } // CollectionInterface col2 = service.openSession() // .flatMap(session -> session.collection("test", "test")) From 9ba88ad7725e0d1905d2c10a6826e95edada1ac3 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:49:02 +0200 Subject: [PATCH 65/74] Reformat --- src/main/java/de/swiesend/secretservice/Prompt.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/Prompt.java b/src/main/java/de/swiesend/secretservice/Prompt.java index 849d0d9..158b1c8 100644 --- a/src/main/java/de/swiesend/secretservice/Prompt.java +++ b/src/main/java/de/swiesend/secretservice/Prompt.java @@ -67,11 +67,15 @@ public Completed await(ObjectPath path, Duration timeout) { if ("/".equals(path.getPath())) { return sh.getLastHandledSignal(Completed.class); } else { - return sh.await(Completed.class, path.getPath(), () -> { + return sh.await( + Completed.class, + path.getPath(), + () -> { prompt(path); return this; }, - timeout); + timeout + ); } } From 853d6f1d877678d8b5027c41ca722da00136e1bd Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sun, 8 Oct 2023 14:58:59 +0200 Subject: [PATCH 66/74] Rewrite prompt handling with Optional --- .../secretservice/handlers/SignalHandler.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java index 8e4d265..c77e7b4 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/SignalHandler.java @@ -11,10 +11,7 @@ import org.slf4j.LoggerFactory; import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; @@ -142,9 +139,9 @@ public S getLastHandledSignal(Class s, String path) { public S await(Class signal, String path, Callable action, Duration timeout) { final int init = count; - Object prompt = null; + Optional maybePrompt = Optional.empty(); try { - prompt = action.call(); + maybePrompt = Optional.ofNullable((Prompt) action.call()); } catch (Exception e) { log.error("Could not acquire a prompt.", e); } @@ -196,12 +193,14 @@ public S await(Class signal, String path, Callable act Thread.currentThread().sleep(DEFAULT_DELAY_MILLIS); } } catch (CancellationException | ExecutionException | InterruptedException | TimeoutException e) { - if (prompt != null && prompt instanceof Prompt) { - ((Prompt) prompt).dismiss(); - log.warn("Cancelled the prompt (" + path + ") manually after exceeding the timeout of " + timeout.getSeconds() + " seconds."); - } else { - log.warn("Cancelled the action, but could not dismiss the prompt.", e); - } + maybePrompt.ifPresentOrElse( + (prompt) -> { + prompt.dismiss(); + log.warn("Cancelled the prompt (" + path + ") manually after exceeding the timeout of " + timeout.getSeconds() + " seconds."); + }, + () -> { + log.warn("Cancelled the action, but could not dismiss the prompt.", e); + }); } finally { handler.cancel(true); executor.shutdownNow(); From 64ab3305e8517d58edde10ec467636726cc3a497 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 11 Nov 2023 16:01:11 +0100 Subject: [PATCH 67/74] Add: default constructor for a Collection without any parameters --- .../secretservice/functional/Collection.java | 37 ++++++++++++------- .../secretservice/functional/Session.java | 4 +- .../functional/integration/Example.java | 29 ++++++++++++++- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index a4766df..e67bc96 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -38,33 +38,44 @@ public class Collection implements CollectionInterface { private ObjectPath path = null; - public Collection(SessionInterface session) { - init(session); + public Collection() { + this(Optional.empty()); + } + + public Collection(Optional maybeSession) { + init(maybeSession); this.path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); this.collection = new de.swiesend.secretservice.Collection(path, connection); this.label = collection.getLabel(); this.id = collection.getId(); } - public Collection(SessionInterface session, String label, Optional maybePassword) { - init(session); + public Collection(String label) { + this(label, Optional.empty(), Optional.empty()); + } + + public Collection(String label, Optional maybePassword) { + this(label, maybePassword, Optional.empty()); + } - this.encryptedCollectionPassword = maybePassword - .flatMap(password -> session.getEncryptedSession().encrypt(password)); + public Collection(String label, Optional maybePassword, Optional maybeSession) { + init(maybeSession); + this.encryptedCollectionPassword = maybePassword.flatMap( + password -> this.session.getEncryptedSession().encrypt(password) + ); // TODO: the constructor may not throw an error... - this.collection = getOrCreateCollection(label) - .orElseThrow(() -> new NoSuchElementException( - String.format("Could not acquire collection with name %s", label) - )); + this.collection = getOrCreateCollection(label).orElseThrow(() -> new NoSuchElementException( + String.format("Could not acquire collection with name %s", label) + )); this.path = collection.getPath(); this.label = Optional.ofNullable(label); this.id = collection.getId(); } - private void init(SessionInterface session) { - this.session = session; + private void init(Optional maybeSession) { + this.session = maybeSession.or(() -> SecretService.create().flatMap(service -> service.openSession())).get(); this.service = session.getService(); this.connection = service.getService().getConnection(); this.timeout = session.getService().getTimeout(); @@ -495,7 +506,7 @@ private Map getLabels() { return labels; } - private boolean exists(String label) { + private boolean existsLabel(String label) { Map labels = getLabels(); return labels.containsValue(label); } diff --git a/src/main/java/de/swiesend/secretservice/functional/Session.java b/src/main/java/de/swiesend/secretservice/functional/Session.java index f193283..3ea80e9 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Session.java +++ b/src/main/java/de/swiesend/secretservice/functional/Session.java @@ -71,14 +71,14 @@ public ServiceInterface getService() { @Override public Optional collection(String label, Optional maybePassword) { - CollectionInterface collection = new Collection(this, label, maybePassword); + CollectionInterface collection = new Collection(label, maybePassword, Optional.of(this)); this.collections.add(collection); return Optional.of(collection); } @Override public Optional defaultCollection() { - CollectionInterface collection = new Collection(this); + CollectionInterface collection = new Collection(Optional.of(this)); this.collections.add(collection); return Optional.of(collection); } diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java index b028cf4..24f7bc3 100644 --- a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -1,5 +1,6 @@ package de.swiesend.secretservice.functional.integration; +import de.swiesend.secretservice.functional.Collection; import de.swiesend.secretservice.functional.SecretService; import de.swiesend.secretservice.functional.interfaces.CollectionInterface; import de.swiesend.secretservice.functional.interfaces.ServiceInterface; @@ -14,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; + public class Example { private static final Logger log = LoggerFactory.getLogger(Example.class); @@ -47,7 +49,7 @@ public void endToEndTest() throws Exception { }*/ // WARN: be careful activating this on the default collection... - if (collectionLabel == "test" || collectionLabel == "täst" ) { + if (collectionLabel == "test" || collectionLabel == "täst") { log.info(String.format("Deleting collection {label: \"%s\", id: \"%s\"} …", collectionLabel, collectionId)); boolean success = collection.delete(); if (success) @@ -99,4 +101,29 @@ public void endToEndTest() throws Exception { } }*/ } + + @Test + public void collection() throws Exception { + char[] secret = null; + + try (CollectionInterface collection = new Collection("test")) { + Map attributes = Map.of("key", "value"); + String item1 = collection.createItem("s1", "s1", attributes).orElseThrow(); + secret = collection.getSecret(item1).orElseThrow(); + + String item2 = collection.createItem("s1", "s2", attributes).orElseThrow(); + collection.getItems(attributes) + .ifPresent(items -> { + for (String it : items) { + collection.getSecret(it).ifPresent(s -> + assertTrue("s1".equals(new String(s)) || "s2".equals(new String(s))) + ); + } + }); + assertTrue(collection.deleteItem(item1)); + assertTrue(collection.deleteItem(item2)); + assertTrue(collection.delete()); + } + assertEquals("s1", new String(secret)); + } } From 4d4b6791a44a7b11db259c1b87b15ebe60915985 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 11 Nov 2023 16:18:15 +0100 Subject: [PATCH 68/74] Add: deleteItem() and deleteItems() --- .../functional/CollectionTest.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java index 89d1502..7309102 100644 --- a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -116,15 +117,25 @@ void deleteCollectionWithoutPassword() { } @Test - @Disabled - // TODO void deleteItem() { + String item = null; + item = collection.createItem("test", "secret").get(); + assertTrue(collection.deleteItem(item)); + assertTrue(collection.getSecret(item).isEmpty()); + + Map attributes = Map.of("key", "value"); + item = collection.createItem("test", "secret", attributes).get(); + assertTrue(collection.deleteItem(item)); + assertTrue(collection.getSecret(item).isEmpty()); } @Test - @Disabled - // TODO void deleteItems() { + String item1 = collection.createItem("test", "secret1").get(); + String item2 = collection.createItem("test", "secret2").get(); + assertTrue(collection.deleteItems(List.of(item1, item2))); + assertTrue(collection.getSecret(item1).isEmpty()); + assertTrue(collection.getSecret(item2).isEmpty()); } @Test From 4ce1817b55c0d55e87db33a46f8312aabe81d440 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 11 Nov 2023 16:25:47 +0100 Subject: [PATCH 69/74] Add: getAttributes() --- .../functional/CollectionTest.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java index 7309102..0d27f85 100644 --- a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.swing.*; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -139,9 +140,21 @@ void deleteItems() { } @Test - @Disabled - // TODO void getAttributes() { + String item = null; + Optional> maybeAttributes; + Map emptyMap = Map.of();; + + item = collection.createItem("test", "secret").get(); + maybeAttributes = collection.getAttributes(item); + assertTrue(maybeAttributes.isPresent()); + assertEquals(emptyMap, maybeAttributes.get()); + + Map attributes = Map.of("key", "value"); + item = collection.createItem("test", "secret", attributes).get(); + maybeAttributes = collection.getAttributes(item); + assertTrue(maybeAttributes.isPresent()); + assertEquals(attributes, maybeAttributes.get()); } @Test From 6bb26e010675d0b7a46374a22f0b401294ff2f89 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Sat, 11 Nov 2023 16:44:12 +0100 Subject: [PATCH 70/74] Add: clear() collection password --- .../de/swiesend/secretservice/Secret.java | 90 +++++++++++-------- .../secretservice/functional/Collection.java | 2 +- .../interfaces/CollectionInterface.java | 5 ++ .../functional/CollectionTest.java | 3 +- 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/Secret.java b/src/main/java/de/swiesend/secretservice/Secret.java index e811afa..7446fa1 100644 --- a/src/main/java/de/swiesend/secretservice/Secret.java +++ b/src/main/java/de/swiesend/secretservice/Secret.java @@ -16,6 +16,11 @@ public final class Secret extends Struct implements AutoCloseable { + // NOTE: the specification defines the default content_type differently with "text/plain; charset=utf8" + // see: https://standards.freedesktop.org/secret-service/0.2/ch14.html + public static final String TEXT_PLAIN_CHARSET_UTF_8 = "text/plain; charset=utf-8"; + public static final String TEXT_PLAIN = "text/plain"; + private static final String CHARSET = "charset="; @Position(0) private final ObjectPath session; @Position(1) @@ -24,15 +29,9 @@ public final class Secret extends Struct implements AutoCloseable { private final byte[] value; @Position(3) private final String contentType; - private String mimeType = null; private Charset charset = null; - // NOTE: the specification defines the default content_type differently with "text/plain; charset=utf8" - // see: https://standards.freedesktop.org/secret-service/0.2/ch14.html - public static final String TEXT_PLAIN_CHARSET_UTF_8 = "text/plain; charset=utf-8"; - public static final String TEXT_PLAIN = "text/plain"; - private static final String CHARSET = "charset="; public Secret(ObjectPath session, byte[] value) { this.session = session; @@ -90,26 +89,6 @@ public Secret(ObjectPath session, byte[] parameters, byte[] value, Charset chars parseContentType(this.contentType); } - private void parseContentType(String contentType) { - String pattern = "[\\s\\\"\\;\\,]"; - List split = Arrays.asList(contentType.split(pattern)); - List filtered = split.stream(). - filter(s -> !(s.isEmpty() || s.length() == 1)). - collect(Collectors.toList()); - - if (filtered.size() > 0) { - mimeType = filtered.get(0); - } else { - mimeType = TEXT_PLAIN; - } - - if (filtered.size() == 2 && filtered.get(1).startsWith(CHARSET)) { - charset = Charset.forName(filtered.get(1).substring(CHARSET.length()).toUpperCase()); - } else { - charset = null; - } - } - static public String createContentType(String mimeType, Charset charset) { return mimeType + "; " + CHARSET + charset.name().toLowerCase(); } @@ -140,7 +119,7 @@ static public byte[] toBytes(char[] passphrase) { } } - static public char[] toChars(byte[] bytes){ + static public char[] toChars(byte[] bytes) { ByteBuffer encoded = ByteBuffer.wrap(bytes); CharBuffer decoded = StandardCharsets.UTF_8.decode(encoded); final char[] chars = new char[decoded.remaining()]; @@ -153,35 +132,70 @@ static public char[] toChars(byte[] bytes){ } } - static public void clear(byte[] bytes) { + static public boolean clear(byte[] bytes) { Arrays.fill(bytes, (byte) 0); - for(byte b: bytes) { - assert((byte) 0 == b); + try { + for (byte b : bytes) { + assert ((byte) 0 == b); + } + } catch (AssertionError e) { + return false; } + return true; } - static public void clear(ByteBuffer buffer) { + static public boolean clear(ByteBuffer buffer) { final byte[] zeros = new byte[buffer.limit()]; Arrays.fill(zeros, (byte) 0); buffer.rewind(); buffer.put(zeros); - for(byte b: buffer.array()) { - assert((byte) 0 == b); + try { + for (byte b : buffer.array()) { + assert ((byte) 0 == b); + } + } catch (AssertionError e) { + return false; } + return true; } - static public void clear(CharBuffer buffer) { + static public boolean clear(CharBuffer buffer) { final char[] zeros = new char[buffer.limit()]; Arrays.fill(zeros, (char) 0); buffer.rewind(); buffer.put(zeros); - for(char c: buffer.array()) { - assert((char) 0 == c); + try { + for (char c : buffer.array()) { + assert ((char) 0 == c); + } + } catch (AssertionError e) { + return false; + } + return true; + } + + private void parseContentType(String contentType) { + String pattern = "[\\s\\\"\\;\\,]"; + List split = Arrays.asList(contentType.split(pattern)); + List filtered = split.stream() + .filter(s -> !(s.isEmpty() || s.length() == 1)) + .collect(Collectors.toList()); + + if (filtered.size() > 0) { + mimeType = filtered.get(0); + } else { + mimeType = TEXT_PLAIN; + } + + if (filtered.size() == 2 && filtered.get(1).startsWith(CHARSET)) { + charset = Charset.forName(filtered.get(1).substring(CHARSET.length()).toUpperCase()); + } else { + charset = null; } } - public void clear() { - clear(value); + public boolean clear() { + return clear(value); } @Override diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index e67bc96..648da6a 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -198,7 +198,7 @@ private Optional performPrompt(ObjectPath path) { @Override public boolean clear() { if (encryptedCollectionPassword.isPresent()) { - encryptedCollectionPassword.get().clear(); + return encryptedCollectionPassword.get().clear(); } return true; } diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java index f12affc..e6ff1db 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java @@ -6,6 +6,11 @@ public interface CollectionInterface extends AutoCloseable { + /** + * Clear the collection password. + * + * @return true, if password could be cleared else false. + */ boolean clear(); /** diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java index 0d27f85..f897710 100644 --- a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -42,9 +42,8 @@ void tearDown() throws Exception { } @Test - @Disabled - // TODO void clear() { + assertTrue(collection.clear()); } @Test From 699315c69fa07e9e1e18b5a83141a67f67549ec6 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Fri, 1 Dec 2023 22:13:56 +0100 Subject: [PATCH 71/74] Add: test for unlockItem() --- .../functional/integration/Example.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java index 24f7bc3..8425199 100644 --- a/src/test/java/de/swiesend/secretservice/functional/integration/Example.java +++ b/src/test/java/de/swiesend/secretservice/functional/integration/Example.java @@ -126,4 +126,18 @@ public void collection() throws Exception { } assertEquals("s1", new String(secret)); } + + @Test + // TODO: add unlockItem() + public void unlockItem() throws Exception { + try (CollectionInterface collection = new Collection("test")) { + Map attributes = Map.of("key", "asdf"); + String item1 = collection.createItem("item-1", "secret", attributes).get(); + collection.lockItem(item1); // TODO: lock item does not apply immediately... + Thread.sleep(1000); + // dismiss the prompt + assert collection.getSecret(item1).isEmpty(); + } + } + } From e052a571192dd4eae56b69ce49642e87001c437b Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Fri, 1 Dec 2023 22:14:38 +0100 Subject: [PATCH 72/74] Add: remaining tests and extend the interface and Collection class --- .../secretservice/functional/Collection.java | 85 +++++++++- .../interfaces/CollectionInterface.java | 51 +++--- .../functional/CollectionTest.java | 158 +++++++++++++----- 3 files changed, 218 insertions(+), 76 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 648da6a..3c5cc0b 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -38,12 +38,20 @@ public class Collection implements CollectionInterface { private ObjectPath path = null; + private boolean clearSessionAtClose = false; + + private boolean isPrompting = true; + public Collection() { this(Optional.empty()); + this.clearSessionAtClose = true; } public Collection(Optional maybeSession) { + if (maybeSession.isEmpty()) { + this.clearSessionAtClose = true; + } init(maybeSession); this.path = Static.Convert.toObjectPath(Static.ObjectPaths.DEFAULT_COLLECTION); this.collection = new de.swiesend.secretservice.Collection(path, connection); @@ -53,13 +61,18 @@ public Collection(Optional maybeSession) { public Collection(String label) { this(label, Optional.empty(), Optional.empty()); + this.clearSessionAtClose = true; } public Collection(String label, Optional maybePassword) { this(label, maybePassword, Optional.empty()); + this.clearSessionAtClose = true; } public Collection(String label, Optional maybePassword, Optional maybeSession) { + if (maybeSession.isEmpty()) { + this.clearSessionAtClose = true; + } init(maybeSession); this.encryptedCollectionPassword = maybePassword.flatMap( password -> this.session.getEncryptedSession().encrypt(password) @@ -75,6 +88,9 @@ public Collection(String label, Optional maybePassword, Optional maybeSession) { + if (maybeSession.isEmpty()) { + this.clearSessionAtClose = true; + } this.session = maybeSession.or(() -> SecretService.create().flatMap(service -> service.openSession())).get(); this.service = session.getService(); this.connection = service.getService().getConnection(); @@ -107,7 +123,10 @@ private Optional createNewCollection(String label) { return Optional.empty(); } } else if (service.isGnomeKeyringAvailable()) { - path = withoutPrompt.createWithMasterPassword(properties, encryptedCollectionPassword.get()).get(); + Optional maybePath = withoutPrompt.createWithMasterPassword(properties, encryptedCollectionPassword.get()); + if (maybePath.isPresent()) { + path = maybePath.get(); + } // TODO: maybe else case? } if (path == null) { @@ -186,6 +205,10 @@ private boolean isDefault() { } private Optional performPrompt(ObjectPath path) { + if (!isPrompting) { + log.trace("dismissed the prompt"); + return Optional.empty(); + } if (!("/".equals(path.getPath()))) { return Optional.ofNullable(prompt.await(path, timeout)) .filter(completed -> !completed.dismissed) @@ -198,9 +221,18 @@ private Optional performPrompt(ObjectPath path) { @Override public boolean clear() { if (encryptedCollectionPassword.isPresent()) { - return encryptedCollectionPassword.get().clear(); + boolean cleared = encryptedCollectionPassword.get().clear(); + encryptedCollectionPassword = Optional.empty(); + if (cleared) { + log.trace("cleared collection password"); + } else { + log.trace("Could not clear collection password"); + } + return cleared; + } else { + log.trace("No collection password to clear"); + return true; } - return true; } @Override @@ -359,6 +391,33 @@ public Optional getId() { return Optional.of(this.collection.getId()); } + @Override + public boolean lockItem(String itemPath) { + Item item = new Item(Static.Convert.toObjectPath(itemPath), service.getService()); + if (!item.isLocked()) { + Pair, ObjectPath> lock = service.getService().lock(List.of(item.getPath())).get(); + java.lang.System.out.println("lock item: " + lock); + de.swiesend.secretservice.interfaces.Prompt.Completed result = prompt.await(lock.b, timeout); + java.lang.System.out.println("lock item prompt: " + result); + } + return true; + } + + @Override + public boolean unlockItem(String itemPath) { + Item item = new Item(Static.Convert.toObjectPath(itemPath), service.getService()); + if (item.isLocked()) { + service.getService().unlock(List.of(item.getPath())).ifPresent(unlock -> { + java.lang.System.out.println("unlock item: " + unlock); + if(unlock.a.isEmpty()){ + de.swiesend.secretservice.interfaces.Prompt.Completed await = prompt.await(unlock.b, timeout); + log.info(String.format("Unlocked Item: %s", await.result.getValue())); + } + }); + } + return true; + } + @Override public Optional getSecret(String objectPath) { if (Static.Utils.isNullOrEmpty(objectPath)) return Optional.empty(); @@ -366,6 +425,7 @@ public Optional getSecret(String objectPath) { return getItem(objectPath) .flatMap(item -> { + // unlockItem(item.getObjectPath()); ObjectPath sessionPath = session.getSession().getPath(); return item.getSecret(sessionPath); }) @@ -487,11 +547,11 @@ public boolean updateItem(String objectPath, String label, CharSequence password @Override public void close() throws Exception { - if (encryptedCollectionPassword.isPresent()) { - encryptedCollectionPassword.get().close(); - log.trace("cleared collection password"); - } + clear(); log.trace("closed collection"); + if (clearSessionAtClose) { + session.close(); + } } private Map getLabels() { @@ -522,4 +582,15 @@ private Optional getItem(String path) { private List lockable() { return Arrays.asList(collection.getPath()); } + + // TODO: add concept to Prompt class.. + public boolean disablePrompt() { + isPrompting = false; + return true; + } + + public boolean enablePrompt() { + isPrompting = true; + return true; + } } diff --git a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java index e6ff1db..cc6de9d 100644 --- a/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java +++ b/src/main/java/de/swiesend/secretservice/functional/interfaces/CollectionInterface.java @@ -16,11 +16,9 @@ public interface CollectionInterface extends AutoCloseable { /** * Create an item in the collection. * - * @param label The label is a description. It is not a unique identifier. - * - * @param secret The secret to be stored. One can encrypt the secret in beforehand, - * to avoid unwanted access from malicious applications. - * + * @param label The label is a description. It is not a unique identifier. + * @param secret The secret to be stored. One can encrypt the secret in beforehand, + * to avoid unwanted access from malicious applications. * @return item path, which is a unique reference generated by the provider */ Optional createItem(String label, CharSequence secret); @@ -28,25 +26,22 @@ public interface CollectionInterface extends AutoCloseable { /** * Create an item in the collection. * - * @param label The label is a description. It is not a unique identifier. - * - * @param secret The secret to be stored. One can encrypt the secret in beforehand, - * to avoid unwanted access from malicious applications. - * - * @param attributes Attributes are provided as a mapping of custom keys and values. - * Applications often identify their secrets using the key `application` - * and their application name as value and providing a unique identifier - * for a specific secret in order to find it by its attributes instead - * of the generated item path. - * - * e.g.: - * - * { - * "application": "APPLICATION_NAME", - * "uuid": "f773dd93-4869-4897-be9d-c53f5b43f904" - * } - * - * + * @param label The label is a description. It is not a unique identifier. + * @param secret The secret to be stored. One can encrypt the secret in beforehand, + * to avoid unwanted access from malicious applications. + * @param attributes Attributes are provided as a mapping of custom keys and values. + * Applications often identify their secrets using the key `application` + * and their application name as value and providing a unique identifier + * for a specific secret in order to find it by its attributes instead + * of the generated item path. + *

+ * e.g.: + * + * { + * "application": "APPLICATION_NAME", + * "uuid": "f773dd93-4869-4897-be9d-c53f5b43f904" + * } + * * @return item path, which is a unique reference generated by the provider */ Optional createItem(String label, CharSequence secret, Map attributes); @@ -83,4 +78,12 @@ public interface CollectionInterface extends AutoCloseable { boolean updateItem(String objectPath, String label, CharSequence password, Map attributes); + boolean lockItem(String objectPath); + + boolean unlockItem(String objectPath); + + boolean disablePrompt(); + + boolean enablePrompt(); + } diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java index f897710..7bcd072 100644 --- a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -2,6 +2,7 @@ import de.swiesend.secretservice.functional.interfaces.CollectionInterface; import de.swiesend.secretservice.functional.interfaces.ServiceInterface; +import de.swiesend.secretservice.functional.interfaces.SessionInterface; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -9,11 +10,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.swing.*; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -23,21 +21,27 @@ class CollectionTest { private static final Logger log = LoggerFactory.getLogger(CollectionTest.class); ServiceInterface service = null; + + SessionInterface session = null; + CollectionInterface collection = null; @BeforeEach void setUp() { service = SecretService.create().get(); - collection = service - .openSession() - .flatMap(session -> session.collection("test-collection", Optional.of("collection"))) - .get(); + session = service.openSession().get(); + try { + collection = session.collection("test-collection", Optional.of("password")).get(); + } catch (NoSuchElementException e) { + collection = session.collection("test-collection", Optional.empty()).get(); + } } @AfterEach void tearDown() throws Exception { collection.delete(); collection.close(); + session.close(); service.close(); } @@ -99,7 +103,6 @@ void deleteALockedCollection() { } @Test - @Disabled void deleteWithALockedService() { assertTrue(service.getService().lockService()); assertTrue(collection.delete()); @@ -107,12 +110,13 @@ void deleteWithALockedService() { @Test @Disabled - // TODO: test password abort + // TODO: test password abort void deleteCollectionWithoutPassword() { - CollectionInterface collectionWithoutPassword = service - .openSession() - .flatMap(session -> session.collection("test-no-password-collection", Optional.empty())) - .get(); + CollectionInterface collectionWithoutPassword = service.openSession() + .flatMap(session -> + session.collection("test-no-password-collection", Optional.empty()) + ).get(); + collectionWithoutPassword.disablePrompt(); assertTrue(collectionWithoutPassword.delete()); } @@ -142,95 +146,159 @@ void deleteItems() { void getAttributes() { String item = null; Optional> maybeAttributes; - Map emptyMap = Map.of();; + Map emptyMap = Map.of(); + ; item = collection.createItem("test", "secret").get(); maybeAttributes = collection.getAttributes(item); assertTrue(maybeAttributes.isPresent()); - assertEquals(emptyMap, maybeAttributes.get()); + assertEquals(emptyMap, maybeAttributes.get()); Map attributes = Map.of("key", "value"); item = collection.createItem("test", "secret", attributes).get(); maybeAttributes = collection.getAttributes(item); assertTrue(maybeAttributes.isPresent()); - assertEquals(attributes, maybeAttributes.get()); + assertEquals(attributes, maybeAttributes.get()); } @Test - @Disabled - // TODO - void getItems() { + void getItems() throws InterruptedException { + Map attributes = Map.of("key", "value-1"); + String item1 = collection.createItem("item-1", "secret", attributes).get(); + String item2 = collection.createItem("item-2", "secret", Map.of("key", "value-2")).get(); + String item3 = collection.createItem("item-3", "secret", attributes).get(); + Optional> maybeItems = collection.getItems(attributes); + assertTrue(maybeItems.isPresent()); + List items = maybeItems.get(); + assertEquals(2, items.size()); } @Test - @Disabled - // TODO void getItemLabel() { + String item = collection.createItem("item-1", "secret").get(); + Optional maybeLabel = collection.getItemLabel(item); + assertTrue(maybeLabel.isPresent()); + assertEquals("item-1", maybeLabel.get()); } @Test @Disabled - // TODO void setItemLabel() { + String item = collection.createItem("item-original", "secret").get(); + assertTrue(collection.setItemLabel(item, "item-override")); + Optional maybeLabel = collection.getItemLabel(item); + assertTrue(maybeLabel.isPresent()); + assertEquals("item-override", maybeLabel.get()); } @Test - @Disabled - // TODO void setLabel() { + assertEquals("test-collection", collection.getLabel().get()); + assertTrue(collection.setLabel("override")); + assertEquals("override", collection.getLabel().get()); } @Test - @Disabled - // TODO void getLabel() { + assertEquals("test-collection", collection.getLabel().get()); } @Test - @Disabled - // TODO void getId() { + assertEquals("test_2dcollection", collection.getId().get()); } + // collection.lockItem(item1); // TODO: test lockItem() + @Test - @Disabled - // TODO void getSecret() { + String item1 = collection.createItem("item-1", "secret-1").get(); + String item2 = collection.createItem("item-2", "secret-2").get(); + + Optional maybeSecret2 = collection.getSecret(item2); + assertTrue(maybeSecret2.isPresent()); + assertEquals("secret-2", new String(maybeSecret2.get())); + + Optional maybeSecret1 = collection.getSecret(item1); + assertTrue(maybeSecret1.isPresent()); + assertEquals("secret-1", new String(maybeSecret1.get())); } @Test - @Disabled - // TODO void getSecrets() { + Map attributes = Map.of("key", "value-1"); + String item1 = collection.createItem("item-1", "secret-1", attributes).get(); + String item2 = collection.createItem("item-2", "secret-2", Map.of("key", "value-2")).get(); + String item3 = collection.createItem("item-3", "secret-3", attributes).get(); + Optional> maybeSecrets = collection.getSecrets(); + assertTrue(maybeSecrets.isPresent()); + assertEquals(3, maybeSecrets.get().size()); + assertEquals(Map.of( + item1, "secret-1", + item2, "secret-2", + item3, "secret-3" + ), maybeSecrets.map(m -> m + .entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> String.valueOf(e.getValue()) + )) + ).get()); } @Test - @Disabled - // TODO void isLocked() { + String item1 = collection.createItem("item-1", "secret-1").get(); + + assertTrue(!collection.isLocked()); + assertTrue(collection.lock()); + assertTrue(collection.isLocked()); + + Optional maybeItem1 = collection.getSecret(item1); + assertTrue(maybeItem1.isPresent()); + assertTrue(!collection.isLocked()); } @Test - @Disabled - // TODO void lock() { + assertTrue(collection.lock()); + assertTrue(collection.isLocked()); } @Test - @Disabled - // TODO void unlockWithUserPermission() { + collection.lock(); + assertTrue(collection.unlockWithUserPermission()); + assertTrue(!collection.isLocked()); } @Test - @Disabled - // TODO void updateItem() { + Map attributes = Map.of("key", "value-1"); + String item1 = collection.createItem("item-1", "secret-1", attributes).get(); + + Map attributesOverride = Map.of("key", "value-override"); + assertTrue(collection.updateItem(item1, "item-1-override", "secret-1-override", attributesOverride)); + assertEquals(0, collection.getItems(attributes).get().size()); + List updatedItems = collection.getItems(attributesOverride).get(); + assertEquals(1, updatedItems.size()); + assertEquals(item1, updatedItems.get(0)); + assertEquals("item-1-override", collection.getItemLabel(item1).get()); + assertEquals("secret-1-override", new String(collection.getSecret(item1).get())); + Map actualAttributes = collection.getAttributes(item1).get(); + assertEquals("value-override", actualAttributes.get("key")); + if (actualAttributes.containsKey("xdg:schema")) { + assertEquals("org.freedesktop.Secret.Generic", actualAttributes.get("xdg:schema")); + } } @Test - @Disabled - // TODO - void close() { + void close() throws Exception { + collection.close(); + assertTrue(collection.lock()); + collection.disablePrompt(); + assertTrue(!collection.unlockWithUserPermission()); + collection.enablePrompt(); } } \ No newline at end of file From 38aa004486f1e831c4866f8f51c14e5b60e91a98 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 5 Dec 2023 22:37:48 +0100 Subject: [PATCH 73/74] Add: early exit, when already closed --- .../secretservice/functional/Collection.java | 13 +++++++--- .../functional/SecretService.java | 26 +++++++++++-------- .../secretservice/functional/Session.java | 12 ++++++--- .../secretservice/functional/System.java | 2 +- .../handlers/MessageHandler.java | 5 ++-- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 3c5cc0b..3fcbe8e 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -42,6 +42,8 @@ public class Collection implements CollectionInterface { private boolean isPrompting = true; + private boolean isClosed = false; + public Collection() { this(Optional.empty()); @@ -547,11 +549,14 @@ public boolean updateItem(String objectPath, String label, CharSequence password @Override public void close() throws Exception { - clear(); - log.trace("closed collection"); - if (clearSessionAtClose) { - session.close(); + if (!isClosed) { + clear(); + if (clearSessionAtClose) { + session.close(); + } } + log.trace("closed collection"); + isClosed = true; } private Map getLabels() { diff --git a/src/main/java/de/swiesend/secretservice/functional/SecretService.java b/src/main/java/de/swiesend/secretservice/functional/SecretService.java index c764115..3a49cd3 100644 --- a/src/main/java/de/swiesend/secretservice/functional/SecretService.java +++ b/src/main/java/de/swiesend/secretservice/functional/SecretService.java @@ -17,15 +17,15 @@ public class SecretService extends ServiceInterface { private static final Logger log = LoggerFactory.getLogger(SecretService.class); + private static Optional maybeSystem = Optional.empty(); private Map sessions = new HashMap<>(); private de.swiesend.secretservice.Service service; - - private static Optional maybeSystem = Optional.empty(); - private boolean isGnomeKeyringAvailable; private Duration timeout = DEFAULT_PROMPT_TIMEOUT; + private boolean isClosed = false; + private SecretService(SystemInterface system, AvailableServices available) { this.service = new Service(system.getConnection()); this.isGnomeKeyringAvailable = available.services.contains(Activatable.GNOME_KEYRING); @@ -87,16 +87,20 @@ public void setTimeout(Duration timeout) { @Override public void close() throws Exception { - List values = getSessions(); - if (values != null) { - for (SessionInterface session : values) { - unregisterSession(session); - session.close(); + if (!isClosed) { + List values = getSessions(); + if (values != null) { + for (SessionInterface session : values) { + unregisterSession(session); + session.close(); + } + } + if (maybeSystem.isPresent()) { + maybeSystem.get().close(); } } - if (maybeSystem.isPresent()) { - maybeSystem.get().close(); - } + log.trace("closed service"); + isClosed = true; } public de.swiesend.secretservice.Service getService() { diff --git a/src/main/java/de/swiesend/secretservice/functional/Session.java b/src/main/java/de/swiesend/secretservice/functional/Session.java index 3ea80e9..7a3d3c0 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Session.java +++ b/src/main/java/de/swiesend/secretservice/functional/Session.java @@ -25,6 +25,8 @@ public class Session implements SessionInterface { private UUID id = null; private ServiceInterface service = null; + private boolean isClosed = false; + private Session(ServiceInterface service, TransportEncryption.EncryptedSession encryptedSession) { this.id = UUID.randomUUID(); this.service = service; @@ -85,10 +87,14 @@ public Optional defaultCollection() { @Override public void close() throws Exception { - for (CollectionInterface collection : this.collections) { - collection.close(); + if (!isClosed) { + for (CollectionInterface collection : this.collections) { + collection.close(); + } + encryptedSession.getSession().close(); } - encryptedSession.getSession().close(); + log.trace("closed session"); + isClosed = true; } @Override diff --git a/src/main/java/de/swiesend/secretservice/functional/System.java b/src/main/java/de/swiesend/secretservice/functional/System.java index f1743b8..bcd532c 100644 --- a/src/main/java/de/swiesend/secretservice/functional/System.java +++ b/src/main/java/de/swiesend/secretservice/functional/System.java @@ -57,6 +57,6 @@ synchronized public boolean disconnect() { @Override public void close() throws Exception { - connection.close(); + disconnect(); } } diff --git a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java index 2ee2801..5ee9e92 100644 --- a/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java +++ b/src/main/java/de/swiesend/secretservice/handlers/MessageHandler.java @@ -8,6 +8,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.Arrays; import java.util.Optional; @@ -83,9 +84,9 @@ public Optional send(String service, String path, String iface, String case "org.freedesktop.DBus.Error.InvalidArgs": case "org.freedesktop.DBus.Error.Failed": if (parameters.length == 1) { - log.error(error + ": \"" + parameters[0] + "\""); + log.error(error + ": \"" + parameters[0] + "\"", new DBusException(error)); } else { - log.error(error + ": " + Arrays.deepToString(parameters)); + log.error(error + ": " + Arrays.deepToString(parameters), new DBusException(error)); } return Optional.empty(); case "org.freedesktop.DBus.Local.Disconnected": From ae19623d911e15bb36cdc008b72b72a7d920c1a3 Mon Sep 17 00:00:00 2001 From: Sebastian Wiesendahl Date: Tue, 5 Dec 2023 23:14:59 +0100 Subject: [PATCH 74/74] Change: lock all collections when retrieving all secrets --- .../secretservice/functional/Collection.java | 5 ++++- .../secretservice/functional/CollectionTest.java | 12 ++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/swiesend/secretservice/functional/Collection.java b/src/main/java/de/swiesend/secretservice/functional/Collection.java index 3fcbe8e..b498771 100644 --- a/src/main/java/de/swiesend/secretservice/functional/Collection.java +++ b/src/main/java/de/swiesend/secretservice/functional/Collection.java @@ -504,7 +504,10 @@ private boolean unlock() { @Override public boolean unlockWithUserPermission() { - if (!isUnlockedOnceWithUserPermission && isDefault()) lock(); + // TODO: locking all collections, maybe lock only + // the default collections to protect it from malicious access + if (!isUnlockedOnceWithUserPermission) lock(); + // if (!isUnlockedOnceWithUserPermission && isDefault()) lock(); unlock(); if (collection.isLocked()) { log.error("The collection was not unlocked with user permission."); diff --git a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java index 7bcd072..7d1392e 100644 --- a/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java +++ b/src/test/java/de/swiesend/secretservice/functional/CollectionTest.java @@ -112,11 +112,12 @@ void deleteWithALockedService() { @Disabled // TODO: test password abort void deleteCollectionWithoutPassword() { - CollectionInterface collectionWithoutPassword = service.openSession() - .flatMap(session -> - session.collection("test-no-password-collection", Optional.empty()) - ).get(); - collectionWithoutPassword.disablePrompt(); + CollectionInterface collectionWithoutPassword = service.openSession().flatMap(session -> + session.collection("test-no-password-collection", Optional.empty()) + ).get(); + // collectionWithoutPassword.disablePrompt(); + Optional> maybeSecrets = collectionWithoutPassword.getSecrets(); + // assertTrue(maybeSecrets.isEmpty()); assertTrue(collectionWithoutPassword.delete()); } @@ -182,7 +183,6 @@ void getItemLabel() { } @Test - @Disabled void setItemLabel() { String item = collection.createItem("item-original", "secret").get(); assertTrue(collection.setItemLabel(item, "item-override"));