Skip to content
This repository has been archived by the owner on May 6, 2024. It is now read-only.

Secret service #23

Merged
merged 5 commits into from
Sep 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions Nickvision.Aura.Tests/KeyringTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Xunit.Abstractions;

namespace Nickvision.Aura.Tests;
Expand All @@ -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)]
Expand Down
42 changes: 30 additions & 12 deletions Nickvision.Aura/Keyring/Keyring.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,23 @@ private Keyring(Store store)
/// <param name="name">The name of the Store</param>
/// <param name="password">The password of the Store, or null to use password from system's credential manager</param>
/// <returns>The Keyring. If the Keyring exists and cannot be loaded, null will be returned</returns>
/// <remarks>If password is null, Windows Credential Manager will be used on Windows or LibSecret will be used on
/// <remarks>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.</remarks>
public static Keyring? Access(string name, string? password = null)
public static async Task<Keyring?> 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
{
Expand All @@ -55,27 +62,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;
}
}
Expand All @@ -92,9 +102,9 @@ private Keyring(Store store)
/// </summary>
/// <param name="name">The name of the Keyring</param>
/// <returns>True if destroyed, else false</returns>
public static bool Destroy(string name)
public static async Task<bool> DestroyAsync(string name)
{
SystemCredentialManager.DeletePassword(name);
await SystemCredentialManager.DeletePasswordAsync(name);
return Store.Destroy(name);
}

Expand Down Expand Up @@ -127,10 +137,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.
/// </summary>
/// <returns>True if successful, else false</returns>
public bool Destroy()
public async Task<bool> DestroyAsync()
{
if(_store.Destroy())
{
try
{
await SystemCredentialManager.DeletePasswordAsync(Name);
}
catch
{
Console.WriteLine("[AURA] Failed to remove password from system keyring.");
}
Dispose();
return true;
}
Expand Down
12 changes: 6 additions & 6 deletions Nickvision.Aura/Keyring/KeyringDialogController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ public KeyringDialogController(string name, Keyring? keyring)
/// Enables the Keyring
/// </summary>
/// <returns>True if successful, false is Keyring already enabled or error</returns>
public bool EnableKeyring(string? password = null)
public async Task<bool> EnableKeyringAsync(string? password = null)
{
if(Keyring == null)
{
Keyring = Keyring.Access(_keyringName, password);
Keyring = await Keyring.AccessAsync(_keyringName, password);
return Keyring != null;
}
return false;
Expand All @@ -76,11 +76,11 @@ public bool EnableKeyring(string? password = null)
/// Disables the Keyring and destroys its data
/// </summary>
/// <returns>True if successful, false if Keyring already disabled</returns>
public bool DisableKeyring()
public async Task<bool> DisableKeyringAsync()
{
if(Keyring != null)
{
Keyring.Destroy();
await Keyring.DestroyAsync();
Keyring = null;
return true;
}
Expand All @@ -92,11 +92,11 @@ public bool DisableKeyring()
/// </summary>
/// <returns>True if successful, false if Keyring doesn't exist or is unlocked</returns>
/// <remarks>Can only be used if the Keyring is not unlocked. If unlocked, use DisableKeyring()</remarks>
public bool ResetKeyring()
public async Task<bool> ResetKeyringAsync()
{
if (Keyring == null)
{
return Keyring.Destroy(_keyringName);
return await Keyring.DestroyAsync(_keyringName);
}
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion Nickvision.Aura/Keyring/Store.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public static Store Load(string name, string password)
Mode = SqliteOpenMode.ReadWrite,
Pooling = false,
Password = password
}.ConnectionString));
}.ConnectionString));
}
catch
{
Expand Down
81 changes: 50 additions & 31 deletions Nickvision.Aura/Keyring/SystemCredentialManager.cs
Original file line number Diff line number Diff line change
@@ -1,47 +1,41 @@
using DBus.Services.Secrets;
using Meziantou.Framework.Win32;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
nlogozzo marked this conversation as resolved.
Show resolved Hide resolved
using System.Text;
using System.Threading.Tasks;

namespace Nickvision.Aura.Keyring;

/// <summary>
/// Object to access system credential manager
/// </summary>
/// <remarks>Uses Windows Credential Manager on Windows and LibSecret on Linux</remarks>
internal static partial class SystemCredentialManager
/// <remarks>Uses Windows Credential Manager on Windows and DBus Secret Service on Linux</remarks>
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;

/// <summary>
/// Gets keyring's password from credential manager
/// </summary>
/// <param name="name">Keyring name</param>
/// <returns>Keyring password or null if failed to get password</returns>
public static string? GetPassword(string name)
public static async Task<string?> GetPasswordAsync(string name)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return CredentialManager.ReadCredential(name)?.Password ?? null;
}
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;
var items = await GetDBusKeyringItemsAsync(name);
if (items.Length > 0)
{
return Encoding.UTF8.GetString(await items[0].GetSecretAsync());
}
return null;
}
throw new PlatformNotSupportedException();
}
Expand All @@ -51,7 +45,7 @@ internal static partial class SystemCredentialManager
/// </summary>
/// <param name="name">Keyring name</param>
/// <returns>Keyring password or null if failed to set password</returns>
public static string? SetPassword(string name)
public static async Task<string?> SetPasswordAsync(string name)
{
var password = new PasswordGenerator().Next();
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Expand All @@ -61,13 +55,14 @@ 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)
var items = await GetDBusKeyringItemsAsync(name);
if (items.Length > 0)
{
return null;
await items[0].SetSecret(Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8");
return password;
}
var lookupAttributes = new Dictionary<string, string> {{ "application", name.ToLower() }};
await _collection!.CreateItemAsync(name, lookupAttributes, Encoding.UTF8.GetBytes(password), "text/plain; charset=utf8", false);
return password;
}
throw new PlatformNotSupportedException();
Expand All @@ -77,7 +72,7 @@ internal static partial class SystemCredentialManager
/// Deletes keyring's password from credential manager
/// </summary>
/// <param name="name">Keyring name</param>
public static void DeletePassword(string name)
public static async Task DeletePasswordAsync(string name)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Expand All @@ -86,11 +81,35 @@ 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);
var items = await GetDBusKeyringItemsAsync(name);
if (items.Length > 0)
{
await items[0].SetSecret(Array.Empty<byte>(), "text/plain; charset=utf8");
}
return;
}
throw new PlatformNotSupportedException();
}

/// <summary>
/// Gets items from DBus Secret Service Keyring
/// </summary>
/// <param name="attribute">Attribute value to search for</param>
/// <returns>Items Array</returns>
/// <remarks>It is possible for multiple items with the same attribute to exist</remarks>
private static async Task<Item[]> GetDBusKeyringItemsAsync(string attribute)
{
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<string, string> {{ "application", attribute.ToLower() }};
return await _collection.SearchItemsAsync(lookupAttributes);
}
}
13 changes: 8 additions & 5 deletions Nickvision.Aura/Nickvision.Aura.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Nickvision.Aura</PackageId>
<Version>2023.9.2</Version>
<Version>2023.9.3</Version>
<Company>Nickvision</Company>
<Authors>Nickvision</Authors>
<Description>A cross-platform base for Nickvision applications</Description>
Expand All @@ -14,12 +13,15 @@
<Copyright>(c) Nickvision 2021-2023</Copyright>
<PackageProjectUrl>https://nickvision.org</PackageProjectUrl>
<RepositoryUrl>https://github.com/NickvisionApps/Aura</RepositoryUrl>
<PackageReleaseNotes>- Allow Keyring to use system secrets for password
- AppInfo's ShortName and Description are now optional</PackageReleaseNotes>
<PackageReleaseNotes>- Some Keyring methods are now async
- Keyring doesn't need libsecret on Linux anymore
</PackageReleaseNotes>
<PackageIcon>logo-r.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ace4896.DBus.Services.Secrets" Version="1.1.0" />
<PackageReference Include="Markdig" Version="0.31.0" />
<PackageReference Include="Meziantou.Framework.Win32.CredentialManager" Version="1.4.2" />
<PackageReference Include="Tmds.DBus" Version="0.15.0" />
Expand All @@ -28,7 +30,8 @@
</ItemGroup>

<ItemGroup>
<None Include="Resources\logo-r.png" Pack="true" PackagePath="\" />
<None Include="Resources\logo-r.png" Pack="true" PackagePath="\" />
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions Nickvision.Aura/README.md
Original file line number Diff line number Diff line change
@@ -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