From 55e9adc7f67db3e46a3d7e501a22f55296e29bf2 Mon Sep 17 00:00:00 2001 From: Fyodor Sobolev <117388856+fsobolev@users.noreply.github.com> Date: Sun, 10 Sep 2023 02:26:39 +0300 Subject: [PATCH 1/5] Keyring - Use DBus service without libsecret --- Nickvision.Aura.Tests/KeyringTest.cs | 20 ++-- Nickvision.Aura/Keyring/Keyring.cs | 43 ++++++--- .../Keyring/KeyringDialogController.cs | 12 +-- Nickvision.Aura/Keyring/Store.cs | 2 +- .../Keyring/SystemCredentialManager.cs | 91 ++++++++++++------- Nickvision.Aura/Nickvision.Aura.csproj | 2 +- 6 files changed, 113 insertions(+), 57 deletions(-) diff --git a/Nickvision.Aura.Tests/KeyringTest.cs b/Nickvision.Aura.Tests/KeyringTest.cs index de4a591..16464d1 100644 --- a/Nickvision.Aura.Tests/KeyringTest.cs +++ b/Nickvision.Aura.Tests/KeyringTest.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Xunit.Abstractions; namespace Nickvision.Aura.Tests; @@ -12,15 +13,22 @@ public KeyringTest(ITestOutputHelper output) } [SkippableFact] - public void AccessTest() + public async Task AccessTest() { - var keyring = Keyring.Keyring.Access("org.nickvision.aura.test"); - // We want the test to succeed when running locally but skip on GitHub where libsecret keyring is locked + var keyring = await Keyring.Keyring.AccessAsync("org.nickvision.aura.test"); + // We want the test to succeed when running locally but skip on GitHub where system keyring is locked Skip.If(keyring == null); - keyring.Destroy(); - keyring.Dispose(); + await keyring.DestroyAsync(); } - + + [Fact] + public async Task AccessWithPasswordTest() + { + var keyring = await Keyring.Keyring.AccessAsync("org.nickvision.aura.test", "TEST"); + Assert.True(keyring != null); + await keyring.DestroyAsync(); + } + [Theory] [InlineData(4)] [InlineData(16)] diff --git a/Nickvision.Aura/Keyring/Keyring.cs b/Nickvision.Aura/Keyring/Keyring.cs index 3100916..e4320e5 100644 --- a/Nickvision.Aura/Keyring/Keyring.cs +++ b/Nickvision.Aura/Keyring/Keyring.cs @@ -1,3 +1,4 @@ + using System; using System.Collections.Generic; using System.IO; @@ -34,16 +35,23 @@ private Keyring(Store store) /// The name of the Store /// The password of the Store, or null to use password from system's credential manager /// The Keyring. If the Keyring exists and cannot be loaded, null will be returned - /// If password is null, Windows Credential Manager will be used on Windows or LibSecret will be used on + /// If password is null, Windows Credential Manager will be used on Windows or DBus Secret Service will be used on /// Linux to get password for the keyring. If a new Store will be created, it will be encrypted with random password. - public static Keyring? Access(string name, string? password = null) + public static async Task AccessAsync(string name, string? password = null) { - password ??= SystemCredentialManager.GetPassword(name); - // If the password is not null, try to open the Store. + try + { + password ??= await SystemCredentialManager.GetPasswordAsync(name); + } + catch (Exception e) + { + Console.WriteLine(e); + } + // If the password is not null or empty, try to open the Store. // -> If it fails because file not found, create new store with provided password. // -> If it fails for any other reason, return null. // If the password is null, try to create new Store with random password without overwriting. If it fails, return null. - if (password != null) + if (!string.IsNullOrEmpty(password)) { try { @@ -55,27 +63,30 @@ private Keyring(Store store) { return new Keyring(Store.Create(name, password, false)); } - catch + catch (Exception e) { + Console.WriteLine(e); return null; } } - catch + catch (Exception e) { + Console.WriteLine(e); return null; } } try { - password = SystemCredentialManager.SetPassword(name); + password = await SystemCredentialManager.SetPasswordAsync(name); if (password == null) { return null; } return new Keyring(Store.Create(name, password, false)); } - catch + catch (Exception e) { + Console.WriteLine(e); return null; } } @@ -92,9 +103,9 @@ private Keyring(Store store) /// /// The name of the Keyring /// True if destroyed, else false - public static bool Destroy(string name) + public static async Task DestroyAsync(string name) { - SystemCredentialManager.DeletePassword(name); + await SystemCredentialManager.DeletePasswordAsync(name); return Store.Destroy(name); } @@ -127,10 +138,18 @@ protected virtual void Dispose(bool disposing) /// Destroys the Keyring and all its data, including the store. Once this method is called, this object should not be used anymore. /// /// True if successful, else false - public bool Destroy() + public async Task DestroyAsync() { if(_store.Destroy()) { + try + { + await SystemCredentialManager.DeletePasswordAsync(Name); + } + catch + { + Console.WriteLine("[AURA] Failed to remove password from system keyring."); + } Dispose(); return true; } diff --git a/Nickvision.Aura/Keyring/KeyringDialogController.cs b/Nickvision.Aura/Keyring/KeyringDialogController.cs index 261087e..7967f1d 100644 --- a/Nickvision.Aura/Keyring/KeyringDialogController.cs +++ b/Nickvision.Aura/Keyring/KeyringDialogController.cs @@ -62,11 +62,11 @@ public KeyringDialogController(string name, Keyring? keyring) /// Enables the Keyring /// /// True if successful, false is Keyring already enabled or error - public bool EnableKeyring(string? password = null) + public async Task EnableKeyringAsync(string? password = null) { if(Keyring == null) { - Keyring = Keyring.Access(_keyringName, password); + Keyring = await Keyring.AccessAsync(_keyringName, password); return Keyring != null; } return false; @@ -76,11 +76,11 @@ public bool EnableKeyring(string? password = null) /// Disables the Keyring and destroys its data /// /// True if successful, false if Keyring already disabled - public bool DisableKeyring() + public async Task DisableKeyringAsync() { if(Keyring != null) { - Keyring.Destroy(); + await Keyring.DestroyAsync(); Keyring = null; return true; } @@ -92,11 +92,11 @@ public bool DisableKeyring() /// /// True if successful, false if Keyring doesn't exist or is unlocked /// Can only be used if the Keyring is not unlocked. If unlocked, use DisableKeyring() - public bool ResetKeyring() + public async Task ResetKeyringAsync() { if (Keyring == null) { - return Keyring.Destroy(_keyringName); + return await Keyring.DestroyAsync(_keyringName); } return false; } diff --git a/Nickvision.Aura/Keyring/Store.cs b/Nickvision.Aura/Keyring/Store.cs index d9b19d3..902fc0b 100644 --- a/Nickvision.Aura/Keyring/Store.cs +++ b/Nickvision.Aura/Keyring/Store.cs @@ -120,7 +120,7 @@ public static Store Load(string name, string password) Mode = SqliteOpenMode.ReadWrite, Pooling = false, Password = password - }.ConnectionString)); + }.ConnectionString)); } catch { diff --git a/Nickvision.Aura/Keyring/SystemCredentialManager.cs b/Nickvision.Aura/Keyring/SystemCredentialManager.cs index b72f6c4..8fd9816 100644 --- a/Nickvision.Aura/Keyring/SystemCredentialManager.cs +++ b/Nickvision.Aura/Keyring/SystemCredentialManager.cs @@ -1,36 +1,28 @@ +using DBus.Services.Secrets; using Meziantou.Framework.Win32; using System; +using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; namespace Nickvision.Aura.Keyring; /// /// Object to access system credential manager /// -/// Uses Windows Credential Manager on Windows and LibSecret on Linux -internal static partial class SystemCredentialManager +/// Uses Windows Credential Manager on Windows and DBus Secret Service on Linux +internal static class SystemCredentialManager { - [LibraryImport("libsecret-1.so.0", StringMarshalling = StringMarshalling.Utf8)] - private static partial nint secret_schema_new(string name, int flags, nint args); - [LibraryImport("libsecret-1.so.0", StringMarshalling = StringMarshalling.Utf8)] - private static partial string secret_password_lookup_sync(nint schema, nint cancellable, nint error, nint args); - [LibraryImport("libsecret-1.so.0", StringMarshalling = StringMarshalling.Utf8)] - [return:MarshalAs(UnmanagedType.I1)] - private static partial bool secret_password_store_sync(nint schema, nint collection, string label, string password, nint cancellable, nint error, nint args); - [LibraryImport("libsecret-1.so.0", StringMarshalling = StringMarshalling.Utf8)] - [return:MarshalAs(UnmanagedType.I1)] - private static partial bool secret_password_clear_sync(nint schema, nint cancellable, nint error, nint args); - [LibraryImport("libsecret-1.so.0")] - private static partial void secret_schema_unref(nint schema); - - private const int SECRET_SCHEMA_NONE = 0; + private static SecretService? _service; + private static Collection? _collection; /// /// Gets keyring's password from credential manager /// /// Keyring name /// Keyring password or null if failed to get password - public static string? GetPassword(string name) + public static async Task GetPasswordAsync(string name) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -38,10 +30,23 @@ internal static partial class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var schema = secret_schema_new(name, SECRET_SCHEMA_NONE, IntPtr.Zero); - var password = secret_password_lookup_sync(schema, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - secret_schema_unref(schema); - return password; + if (_service == null) + { + _service = await SecretService.ConnectAsync(EncryptionType.Dh); + _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); + } + if (_collection == null) + { + throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); + } + await _collection.UnlockAsync(); + var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + var items = await _collection.SearchItemsAsync(lookupAttributes); + if (items.Length > 0) + { + return Encoding.UTF8.GetString(await items[0].GetSecretAsync()); + } + return null; } throw new PlatformNotSupportedException(); } @@ -51,7 +56,7 @@ internal static partial class SystemCredentialManager /// /// Keyring name /// Keyring password or null if failed to set password - public static string? SetPassword(string name) + public static async Task SetPasswordAsync(string name) { var password = new PasswordGenerator().Next(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -61,13 +66,24 @@ internal static partial class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var schema = secret_schema_new(name, SECRET_SCHEMA_NONE, IntPtr.Zero); - var success = secret_password_store_sync(schema, IntPtr.Zero, name, password, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - secret_schema_unref(schema); - if (!success) + if (_service == null) + { + _service = await SecretService.ConnectAsync(EncryptionType.Dh); + _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); + } + if (_collection == null) + { + throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); + } + await _collection.UnlockAsync(); + var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + var items = await _collection.SearchItemsAsync(lookupAttributes); + if (items.Length > 0) { - return null; + await items[0].SetSecret(Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8"); + return password; } + await _collection.CreateItemAsync(name, lookupAttributes, Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8", false); return password; } throw new PlatformNotSupportedException(); @@ -77,7 +93,7 @@ internal static partial class SystemCredentialManager /// Deletes keyring's password from credential manager /// /// Keyring name - public static void DeletePassword(string name) + public static async Task DeletePasswordAsync(string name) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -86,9 +102,22 @@ public static void DeletePassword(string name) } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var schema = secret_schema_new(name, SECRET_SCHEMA_NONE, IntPtr.Zero); - secret_password_clear_sync(schema, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); - secret_schema_unref(schema); + if (_service == null) + { + _service = await SecretService.ConnectAsync(EncryptionType.Dh); + _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); + } + if (_collection == null) + { + throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); + } + await _collection.UnlockAsync(); + var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + var items = await _collection.SearchItemsAsync(lookupAttributes); + if (items.Length > 0) + { + //await items[0].SetSecret(Array.Empty(), "text/plain; charset=utf8"); + } return; } throw new PlatformNotSupportedException(); diff --git a/Nickvision.Aura/Nickvision.Aura.csproj b/Nickvision.Aura/Nickvision.Aura.csproj index de74ade..3f3c17f 100644 --- a/Nickvision.Aura/Nickvision.Aura.csproj +++ b/Nickvision.Aura/Nickvision.Aura.csproj @@ -3,7 +3,6 @@ net7.0 enable - true Nickvision.Aura 2023.9.2 Nickvision @@ -20,6 +19,7 @@ + From 45fddbc482168874207895aec8fb99118f7f9c89 Mon Sep 17 00:00:00 2001 From: Fyodor Sobolev <117388856+fsobolev@users.noreply.github.com> Date: Sun, 10 Sep 2023 02:43:53 +0300 Subject: [PATCH 2/5] Avoid duplicate code in SystemCredentialManager --- Nickvision.Aura/Keyring/Keyring.cs | 1 - .../Keyring/SystemCredentialManager.cs | 60 +++++++------------ 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/Nickvision.Aura/Keyring/Keyring.cs b/Nickvision.Aura/Keyring/Keyring.cs index e4320e5..a503fcd 100644 --- a/Nickvision.Aura/Keyring/Keyring.cs +++ b/Nickvision.Aura/Keyring/Keyring.cs @@ -1,4 +1,3 @@ - using System; using System.Collections.Generic; using System.IO; diff --git a/Nickvision.Aura/Keyring/SystemCredentialManager.cs b/Nickvision.Aura/Keyring/SystemCredentialManager.cs index 8fd9816..8a4d407 100644 --- a/Nickvision.Aura/Keyring/SystemCredentialManager.cs +++ b/Nickvision.Aura/Keyring/SystemCredentialManager.cs @@ -30,18 +30,7 @@ internal static class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (_service == null) - { - _service = await SecretService.ConnectAsync(EncryptionType.Dh); - _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); - } - if (_collection == null) - { - throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); - } - await _collection.UnlockAsync(); - var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; - var items = await _collection.SearchItemsAsync(lookupAttributes); + var items = await GetDBusKeyringItems(name); if (items.Length > 0) { return Encoding.UTF8.GetString(await items[0].GetSecretAsync()); @@ -66,24 +55,14 @@ internal static class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (_service == null) - { - _service = await SecretService.ConnectAsync(EncryptionType.Dh); - _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); - } - if (_collection == null) - { - throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); - } - await _collection.UnlockAsync(); - var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; - var items = await _collection.SearchItemsAsync(lookupAttributes); + var items = await GetDBusKeyringItems(name); if (items.Length > 0) { await items[0].SetSecret(Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8"); return password; } - await _collection.CreateItemAsync(name, lookupAttributes, Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8", false); + var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + await _collection!.CreateItemAsync(name, lookupAttributes, Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8", false); return password; } throw new PlatformNotSupportedException(); @@ -102,24 +81,29 @@ public static async Task DeletePasswordAsync(string name) } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - if (_service == null) - { - _service = await SecretService.ConnectAsync(EncryptionType.Dh); - _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); - } - if (_collection == null) - { - throw new AuraException("[AURA] Failed to get or create default collection in system keyring."); - } - await _collection.UnlockAsync(); - var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; - var items = await _collection.SearchItemsAsync(lookupAttributes); + var items = await GetDBusKeyringItems(name); if (items.Length > 0) { - //await items[0].SetSecret(Array.Empty(), "text/plain; charset=utf8"); + await items[0].SetSecret(Array.Empty(), "text/plain; charset=utf8"); } return; } throw new PlatformNotSupportedException(); } + + private static async Task GetDBusKeyringItems(string name) + { + if (_service == null) + { + _service = await SecretService.ConnectAsync(EncryptionType.Dh); + _collection = await _service.GetDefaultCollectionAsync() ?? await _service.CreateCollectionAsync("Default keyring", "default"); + } + if (_collection == null) + { + throw new AuraException("Failed to get or create default collection in system keyring."); + } + await _collection.UnlockAsync(); + var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + return await _collection.SearchItemsAsync(lookupAttributes); + } } \ No newline at end of file From 7b43d5b02c6e2a3d67eca7817228a3e5b9567868 Mon Sep 17 00:00:00 2001 From: Fyodor Sobolev <117388856+fsobolev@users.noreply.github.com> Date: Sun, 10 Sep 2023 02:49:12 +0300 Subject: [PATCH 3/5] Add missing comment --- Nickvision.Aura/Keyring/SystemCredentialManager.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Nickvision.Aura/Keyring/SystemCredentialManager.cs b/Nickvision.Aura/Keyring/SystemCredentialManager.cs index 8a4d407..6eccc97 100644 --- a/Nickvision.Aura/Keyring/SystemCredentialManager.cs +++ b/Nickvision.Aura/Keyring/SystemCredentialManager.cs @@ -91,7 +91,13 @@ public static async Task DeletePasswordAsync(string name) throw new PlatformNotSupportedException(); } - private static async Task GetDBusKeyringItems(string name) + /// + /// Gets items from DBus Secret Service Keyring + /// + /// Attribute value to search for + /// Items Array + /// It is possible for multiple items with the same attribute to exist + private static async Task GetDBusKeyringItems(string attribute) { if (_service == null) { @@ -103,7 +109,7 @@ private static async Task GetDBusKeyringItems(string name) throw new AuraException("Failed to get or create default collection in system keyring."); } await _collection.UnlockAsync(); - var lookupAttributes = new Dictionary {{ "application", name.ToLower() }}; + var lookupAttributes = new Dictionary {{ "application", attribute.ToLower() }}; return await _collection.SearchItemsAsync(lookupAttributes); } } \ No newline at end of file From ffd59b8eb5901c200897070c7d1c55cb2f6e27f0 Mon Sep 17 00:00:00 2001 From: Fyodor Sobolev <117388856+fsobolev@users.noreply.github.com> Date: Sun, 10 Sep 2023 02:57:41 +0300 Subject: [PATCH 4/5] Update version and changelog, add README to package --- Nickvision.Aura/Nickvision.Aura.csproj | 11 +++++++---- Nickvision.Aura/README.md | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 Nickvision.Aura/README.md diff --git a/Nickvision.Aura/Nickvision.Aura.csproj b/Nickvision.Aura/Nickvision.Aura.csproj index 3f3c17f..2cb5e3d 100644 --- a/Nickvision.Aura/Nickvision.Aura.csproj +++ b/Nickvision.Aura/Nickvision.Aura.csproj @@ -4,7 +4,7 @@ net7.0 enable Nickvision.Aura - 2023.9.2 + 2023.9.3 Nickvision Nickvision A cross-platform base for Nickvision applications @@ -13,9 +13,11 @@ (c) Nickvision 2021-2023 https://nickvision.org https://github.com/NickvisionApps/Aura - - Allow Keyring to use system secrets for password - - AppInfo's ShortName and Description are now optional + - Some Keyring methods are now async + - Keyring doesn't need libsecret on Linux anymore + logo-r.png + README.md @@ -28,7 +30,8 @@ - + + diff --git a/Nickvision.Aura/README.md b/Nickvision.Aura/README.md new file mode 100644 index 0000000..a07cb32 --- /dev/null +++ b/Nickvision.Aura/README.md @@ -0,0 +1,8 @@ + **A cross-platform base for Nickvision applications** + + Aura provides the following functionality: + 1. Stores application information: name, version, changelog etc + 2. Allows to load and save configuration files in JSON format + 3. Can start an IPC server using named pipe. In this case, an application becomes single-instance. New instances will send command-line arguments to existing one and quit. + 4. Access system's network status and listen for changes + 5. Store credentials in a secure fashion \ No newline at end of file From 840552ffa8cd80ccf436724baf5fc8bf81bef0c5 Mon Sep 17 00:00:00 2001 From: Fyodor Sobolev <117388856+fsobolev@users.noreply.github.com> Date: Sun, 10 Sep 2023 03:13:19 +0300 Subject: [PATCH 5/5] GetDBusKeyringItems -> GetDBusKeyringItemsAsync --- Nickvision.Aura/Keyring/SystemCredentialManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Nickvision.Aura/Keyring/SystemCredentialManager.cs b/Nickvision.Aura/Keyring/SystemCredentialManager.cs index 6eccc97..172aefb 100644 --- a/Nickvision.Aura/Keyring/SystemCredentialManager.cs +++ b/Nickvision.Aura/Keyring/SystemCredentialManager.cs @@ -30,7 +30,7 @@ internal static class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var items = await GetDBusKeyringItems(name); + var items = await GetDBusKeyringItemsAsync(name); if (items.Length > 0) { return Encoding.UTF8.GetString(await items[0].GetSecretAsync()); @@ -55,7 +55,7 @@ internal static class SystemCredentialManager } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var items = await GetDBusKeyringItems(name); + var items = await GetDBusKeyringItemsAsync(name); if (items.Length > 0) { await items[0].SetSecret(Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8"); @@ -81,7 +81,7 @@ public static async Task DeletePasswordAsync(string name) } if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - var items = await GetDBusKeyringItems(name); + var items = await GetDBusKeyringItemsAsync(name); if (items.Length > 0) { await items[0].SetSecret(Array.Empty(), "text/plain; charset=utf8"); @@ -97,7 +97,7 @@ public static async Task DeletePasswordAsync(string name) /// Attribute value to search for /// Items Array /// It is possible for multiple items with the same attribute to exist - private static async Task GetDBusKeyringItems(string attribute) + private static async Task GetDBusKeyringItemsAsync(string attribute) { if (_service == null) {