diff --git a/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs b/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs
index 4e0ec61..31ac4c7 100644
--- a/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs
+++ b/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs
@@ -101,10 +101,11 @@ private SecretServiceProxy(DBusConnection connection, string sessionPath)
}
///
- /// Unlocks the given object (collection or item path).
+ /// Unlocks the given object (collection or item path), prompting the user if required.
///
/// The D-Bus object path to unlock
- internal async Task UnlockAsync(string objectPath)
+ /// True if unlocked successfully, false if the user dismissed the prompt
+ internal async Task UnlockAsync(string objectPath)
{
MessageBuffer buffer;
{
@@ -113,14 +114,57 @@ internal async Task UnlockAsync(string objectPath)
writer.WriteArray(new ObjectPath[] { objectPath });
buffer = writer.CreateMessage();
}
- await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
+ var promptPath = await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
{
var reader = m.GetBodyReader();
reader.AlignStruct();
- reader.ReadArrayOfObjectPath(); // unlocked (ignored)
- reader.ReadObjectPath(); // prompt (ignored)
- return true;
+ reader.ReadArrayOfObjectPath(); // already-unlocked list (not reliable for items that need prompting)
+ return reader.ReadObjectPathAsString(); // prompt path, or "/" if no prompt is needed
}, null);
+ if (string.IsNullOrEmpty(promptPath) || promptPath == "/")
+ {
+ // Object was already unlocked, no user prompt required
+ return true;
+ }
+ return await PromptAsync(promptPath);
+ }
+
+ ///
+ /// Invokes a Secret Service prompt and waits for the user to complete or dismiss it.
+ ///
+ /// The D-Bus object path of the prompt
+ /// True if the user completed the prompt, false if dismissed
+ private async Task PromptAsync(string promptPath)
+ {
+ var tcs = new TaskCompletionSource();
+ using var subscription = await _connection.WatchSignalAsync(
+ SecretsBus, promptPath, "org.freedesktop.Secret.Prompt", "Completed",
+ static (Message m, object? _) =>
+ {
+ var reader = m.GetBodyReader();
+ return reader.ReadBool(); // dismissed
+ },
+ (Exception? ex, bool dismissed) =>
+ {
+ if (ex is not null)
+ {
+ tcs.TrySetException(ex);
+ }
+ else
+ {
+ tcs.TrySetResult(!dismissed);
+ }
+ },
+ null, /* emitOnCapturedContext */ false, ObserverFlags.None);
+ MessageBuffer buffer;
+ {
+ using var writer = _connection.GetMessageWriter();
+ writer.WriteMethodCallHeader(SecretsBus, promptPath, "org.freedesktop.Secret.Prompt", "Prompt", "s", MessageFlags.None);
+ writer.WriteString(""); // no parent window-id
+ buffer = writer.CreateMessage();
+ }
+ await _connection.CallMethodAsync(buffer);
+ return await tcs.Task;
}
///
@@ -167,17 +211,16 @@ await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
}
///
- /// Searches for items in the specified collection that match the given attributes.
+ /// Searches for items across all collections that match the given attributes.
///
- /// The collection object path
/// Attributes to match
- /// An array of matching item object paths
- internal async Task SearchItemsAsync(string collectionPath, Dictionary attributes)
+ /// A tuple of unlocked and locked item object paths
+ internal async Task<(string[] Unlocked, string[] Locked)> SearchItemsAsync(Dictionary attributes)
{
MessageBuffer buffer;
{
using var writer = _connection.GetMessageWriter();
- writer.WriteMethodCallHeader(SecretsBus, collectionPath, CollectionInterface, "SearchItems", "a{ss}", MessageFlags.None);
+ writer.WriteMethodCallHeader(SecretsBus, SecretsPath, ServiceInterface, "SearchItems", "a{ss}", MessageFlags.None);
var dictStart = writer.WriteDictionaryStart();
foreach (var kv in attributes)
{
@@ -191,13 +234,20 @@ internal async Task SearchItemsAsync(string collectionPath, Dictionary
return await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
{
var reader = m.GetBodyReader();
- var paths = reader.ReadArrayOfObjectPath();
- var result = new string[paths.Length];
- for (var i = 0; i < paths.Length; i++)
+ reader.AlignStruct();
+ var unlockedPaths = reader.ReadArrayOfObjectPath();
+ var lockedPaths = reader.ReadArrayOfObjectPath();
+ var unlocked = new string[unlockedPaths.Length];
+ var locked = new string[lockedPaths.Length];
+ for (var i = 0; i < unlockedPaths.Length; i++)
{
- result[i] = paths[i].ToString();
+ unlocked[i] = unlockedPaths[i].ToString();
}
- return result;
+ for (var i = 0; i < lockedPaths.Length; i++)
+ {
+ locked[i] = lockedPaths[i].ToString();
+ }
+ return (unlocked, locked);
}, null);
}
@@ -215,16 +265,27 @@ internal async Task SearchItemsAsync(string collectionPath, Dictionary
writer.WriteObjectPath(_sessionPath);
buffer = writer.CreateMessage();
}
- return await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
+ try
{
- var reader = m.GetBodyReader();
- reader.AlignStruct();
- reader.ReadObjectPath(); // session (ignored)
- reader.ReadArrayOfByte(); // parameters (ignored for plain)
- var valueBytes = reader.ReadArrayOfByte();
- reader.ReadString(); // content type (ignored)
- return Encoding.UTF8.GetString(valueBytes);
- }, null);
+ return await _connection.CallMethodAsync(buffer, static (Message m, object? _) =>
+ {
+ var reader = m.GetBodyReader();
+ reader.AlignStruct();
+ reader.ReadObjectPath(); // session (ignored)
+ reader.ReadArrayOfByte(); // parameters (ignored for plain)
+ var valueBytes = reader.ReadArrayOfByte();
+ reader.ReadString(); // content type (ignored)
+ return Encoding.UTF8.GetString(valueBytes);
+ }, null);
+ }
+ catch (DBusErrorReplyException e)
+ {
+ if(e.ErrorName == "org.freedesktop.Secret.Error.IsLocked")
+ {
+ return null;
+ }
+ }
+ return null;
}
///
diff --git a/Nickvision.Desktop/Nickvision.Desktop.csproj b/Nickvision.Desktop/Nickvision.Desktop.csproj
index a6e1359..f96f0ba 100644
--- a/Nickvision.Desktop/Nickvision.Desktop.csproj
+++ b/Nickvision.Desktop/Nickvision.Desktop.csproj
@@ -8,7 +8,7 @@
true
true
Nickvision.Desktop
- 2026.3.9
+ 2026.3.10
Nickvision
Nickvision
A cross-platform base for Nickvision desktop applications.
diff --git a/Nickvision.Desktop/System/SecretService.cs b/Nickvision.Desktop/System/SecretService.cs
index 8bc9317..3c15b05 100644
--- a/Nickvision.Desktop/System/SecretService.cs
+++ b/Nickvision.Desktop/System/SecretService.cs
@@ -132,9 +132,7 @@ public async Task AddAsync(Secret secret)
return false;
}
await svc.UnlockAsync(collPath);
- var itemPath = await svc.CreateItemAsync(collPath, secret.Name,
- new Dictionary { { "application", secret.Name } },
- secret.Value);
+ var itemPath = await svc.CreateItemAsync(collPath, secret.Name, new Dictionary { { "application", secret.Name } }, secret.Value);
var res = !string.IsNullOrEmpty(itemPath);
if (res)
{
@@ -245,29 +243,21 @@ public async Task DeleteAsync(string name)
_logger.LogError($"Failed to delete system secret ({name}): unable to connect to secrets service.");
return false;
}
- var collPath = await svc.GetDefaultCollectionPathAsync();
- if (string.IsNullOrEmpty(collPath) || collPath == "/")
- {
- collPath = await svc.CreateCollectionAsync("Default keyring", "default");
- }
- if (string.IsNullOrEmpty(collPath))
- {
- _logger.LogError($"Failed to delete system secret ({name}) as the keyring collection could not be accessed.");
- return false;
- }
- await svc.UnlockAsync(collPath);
- var items = await svc.SearchItemsAsync(collPath,
- new Dictionary { { "application", name } });
- if (items.Length == 0)
+ var (unlocked, locked) = await svc.SearchItemsAsync(new Dictionary { { "application", name } });
+ var itemPath = unlocked.Length > 0 ? unlocked[0] : locked.Length > 0 ? locked[0] : null;
+ if (itemPath is null)
{
_logger.LogWarning($"System secret ({name}) not found.");
+ return false;
}
- else
+ if (locked.Length > 0 && !await svc.UnlockAsync(itemPath))
{
- await svc.DeleteItemAsync(items[0]);
- _logger.LogInformation($"Deleted system secret ({name}) successfully.");
+ _logger.LogError($"Failed to delete system secret ({name}): the user dismissed the unlock prompt.");
+ return false;
}
- return items.Length > 0;
+ await svc.DeleteItemAsync(itemPath);
+ _logger.LogInformation($"Deleted system secret ({name}) successfully.");
+ return true;
}
else
{
@@ -356,25 +346,27 @@ public async Task DeleteAsync(string name)
_logger.LogError($"Failed to get system secret ({name}): unable to connect to secrets service.");
return null;
}
- var collPath = await svc.GetDefaultCollectionPathAsync();
- if (string.IsNullOrEmpty(collPath) || collPath == "/")
+ var (unlocked, locked) = await svc.SearchItemsAsync(new Dictionary { { "application", name } });
+ string? itemPath = null;
+ if (unlocked.Length > 0)
{
- collPath = await svc.CreateCollectionAsync("Default keyring", "default");
+ itemPath = unlocked[0];
}
- if (string.IsNullOrEmpty(collPath))
+ else if (locked.Length > 0)
{
- _logger.LogError($"Failed to get system secret ({name}) as the keyring collection could not be accessed.");
- return null;
+ if (!await svc.UnlockAsync(locked[0]))
+ {
+ _logger.LogError($"Failed to get system secret ({name}): the user dismissed the unlock prompt.");
+ return null;
+ }
+ itemPath = locked[0];
}
- await svc.UnlockAsync(collPath);
- var items = await svc.SearchItemsAsync(collPath,
- new Dictionary { { "application", name } });
- if (items.Length == 0)
+ if (itemPath is null)
{
_logger.LogInformation($"System secret ({name}) not found.");
return null;
}
- var value = await svc.GetSecretAsync(items[0]);
+ var value = await svc.GetSecretAsync(itemPath);
return value is null ? null : new Secret(name, value);
}
else
@@ -476,29 +468,29 @@ public async Task UpdateAsync(Secret secret)
_logger.LogError($"Failed to update system secret ({secret.Name}): unable to connect to secrets service.");
return false;
}
- var collPath = await svc.GetDefaultCollectionPathAsync();
- if (string.IsNullOrEmpty(collPath) || collPath == "/")
+ var (unlocked, locked) = await svc.SearchItemsAsync(new Dictionary { { "application", secret.Name } });
+ string? itemPath = null;
+ if (unlocked.Length > 0)
{
- collPath = await svc.CreateCollectionAsync("Default keyring", "default");
+ itemPath = unlocked[0];
}
- if (string.IsNullOrEmpty(collPath))
+ else if (locked.Length > 0)
{
- _logger.LogError($"Failed to update system secret ({secret.Name}) as the keyring collection could not be accessed.");
- return false;
+ if (!await svc.UnlockAsync(locked[0]))
+ {
+ _logger.LogError($"Failed to update system secret ({secret.Name}): the user dismissed the unlock prompt.");
+ return false;
+ }
+ itemPath = locked[0];
}
- await svc.UnlockAsync(collPath);
- var items = await svc.SearchItemsAsync(collPath,
- new Dictionary { { "application", secret.Name } });
- if (items.Length == 0)
+ if (itemPath is null)
{
_logger.LogError($"Failed to update system secret ({secret.Name}).");
+ return false;
}
- else
- {
- await svc.SetSecretAsync(items[0], secret.Value);
- _logger.LogInformation($"Updated system secret ({secret.Name}) successfully.");
- }
- return items.Length > 0;
+ await svc.SetSecretAsync(itemPath, secret.Value);
+ _logger.LogInformation($"Updated system secret ({secret.Name}) successfully.");
+ return true;
}
else
{