diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 5b64dc0..8c0ba83 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -13,7 +13,7 @@ updates:
directory: "/"
schedule:
interval: monthly
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 1
registries:
- nuget-feed-nuget-pkg-github-com-ashleighadams-index-json
- public
@@ -26,4 +26,4 @@ updates:
- "patch"
ignore:
- dependency-name: "*"
- update-types: ["version-update:semver-major"]
\ No newline at end of file
+ update-types: ["version-update:semver-major"]
diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 099ade2..4dcf95e 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -45,12 +45,12 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.x
+ dotnet-version: 10.x
- name: Setup Verlite
run: |
#verlite_version="$(dotnet msbuild Directory.Build.props -getitem:PackageReference | jq -r '.Items.PackageReference[] | select(.Identity=="Verlite.MsBuild") .Version')"
- verlite_version="2.5.0"
+ verlite_version="2.7.0"
dotnet tool install --global Verlite.CLI --version "$verlite_version"
verlite . --enable-shadow-repo --auto-fetch --verbosity verbatim
@@ -97,12 +97,12 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.x
+ dotnet-version: 10.x
- name: Setup Verlite
run: |
#verlite_version="$(dotnet msbuild Directory.Build.props -getitem:PackageReference | jq -r '.Items.PackageReference[] | select(.Identity=="Verlite.MsBuild") .Version')"
- verlite_version="2.5.0"
+ verlite_version="2.7.0"
dotnet tool install --global Verlite.CLI --version "$verlite_version"
verlite . --enable-shadow-repo --auto-fetch --verbosity verbatim
diff --git a/.github/workflows/deploy-release.yml b/.github/workflows/deploy-release.yml
index eddd701..1e7c72e 100644
--- a/.github/workflows/deploy-release.yml
+++ b/.github/workflows/deploy-release.yml
@@ -31,9 +31,9 @@ jobs:
# - rid: linux-arm64
# os: ubuntu-latest
# project: src/Keyden.Desktop.Unix
- - rid: osx-x64
- os: macos-13
- project: src/Keyden.Desktop.Unix
+ #- rid: osx-x64
+ # os: macos-13
+ # project: src/Keyden.Desktop.Unix
# - rid: osx-arm64
# os: macos-13
# project: src/Keyden.Desktop.Unix
@@ -46,12 +46,12 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.x
+ dotnet-version: 10.x
- name: Setup Verlite
run: |
#verlite_version="$(dotnet msbuild Directory.Build.props -getitem:PackageReference | jq -r '.Items.PackageReference[] | select(.Identity=="Verlite.MsBuild") .Version')"
- verlite_version="2.5.0"
+ verlite_version="2.7.0"
dotnet tool install --global Verlite.CLI --version "$verlite_version"
verlite . --enable-shadow-repo --auto-fetch --verbosity verbatim
diff --git a/Directory.Build.props b/Directory.Build.props
index 9827602..25aae6c 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -7,7 +7,7 @@
enable
- 13.0
+ 14.0
@@ -25,7 +25,7 @@
true
true
- 9.0
+ 10.0
true
@@ -65,7 +65,7 @@
true
-
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 453b199..0629633 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,30 +4,31 @@
true
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
\ No newline at end of file
diff --git a/Keyden.sln b/Keyden.sln
index 3fbab7b..6de6c95 100644
--- a/Keyden.sln
+++ b/Keyden.sln
@@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{7CFFD7
.github\workflows\deploy-release.yml = .github\workflows\deploy-release.yml
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Keyden.1Password.Sdk", "src\Keyden.1Password.Sdk\Keyden.1Password.Sdk.csproj", "{10A5765F-A07B-33D4-F164-85AAFF3181E3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -168,6 +170,26 @@ Global
{9D7AA409-A924-484E-9E25-85795D1D013F}.Release|x64.Build.0 = Release|x64
{9D7AA409-A924-484E-9E25-85795D1D013F}.Release|x86.ActiveCfg = Release|x86
{9D7AA409-A924-484E-9E25-85795D1D013F}.Release|x86.Build.0 = Release|x86
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|ARM.Build.0 = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|x64.Build.0 = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Debug|x86.Build.0 = Debug|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|ARM.ActiveCfg = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|ARM.Build.0 = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|ARM64.Build.0 = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|x64.ActiveCfg = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|x64.Build.0 = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|x86.ActiveCfg = Release|Any CPU
+ {10A5765F-A07B-33D4-F164-85AAFF3181E3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/global.json b/global.json
index 10054b8..4d1a435 100644
--- a/global.json
+++ b/global.json
@@ -1,6 +1,6 @@
{
"sdk": {
- "version": "9.0.0",
+ "version": "10.0.0",
"rollForward": "latestFeature"
}
}
diff --git a/src/Keyden.1Password.Sdk/Base64JsonConverter.cs b/src/Keyden.1Password.Sdk/Base64JsonConverter.cs
new file mode 100644
index 0000000..e840985
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Base64JsonConverter.cs
@@ -0,0 +1,18 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK;
+
+internal class Base64JsonConverter : JsonConverter
+{
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ return OnePassSdkNative.Utf8.GetString(reader.GetBytesFromBase64());
+ }
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ writer.WriteBase64StringValue(OnePassSdkNative.Utf8.GetBytes(value));
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/ByteArrayJsonConverter.cs b/src/Keyden.1Password.Sdk/ByteArrayJsonConverter.cs
new file mode 100644
index 0000000..d0e813b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/ByteArrayJsonConverter.cs
@@ -0,0 +1,40 @@
+using System;
+using System.IO;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK;
+
+internal class ByteArrayJsonConverter : JsonConverter
+{
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var ms = new MemoryStream();
+
+ if (reader.TokenType != JsonTokenType.StartArray)
+ throw new JsonException($"Expected start array, got {reader.TokenType}", null, null, reader.TokenStartIndex);
+
+ while (reader.Read() && reader.TokenType == JsonTokenType.Number)
+ {
+ if (!reader.TryGetByte(out byte x))
+ throw new JsonException($"Expected number between [0..255]", null, null, reader.TokenStartIndex);
+ ms.WriteByte(x);
+ }
+
+ if (reader.TokenType != JsonTokenType.EndArray)
+ throw new JsonException($"Expected end array, got {reader.TokenType}", null, null, reader.TokenStartIndex);
+
+ ms.Flush();
+ return OnePassSdkNative.Utf8.GetString(ms.ToArray());
+ }
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ var bytes = OnePassSdkNative.Utf8.GetBytes(value);
+
+ writer.WriteStartArray();
+ foreach (byte x in bytes)
+ writer.WriteNumberValue(x);
+ writer.WriteEndArray();
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Keyden.1Password.Sdk.csproj b/src/Keyden.1Password.Sdk/Keyden.1Password.Sdk.csproj
new file mode 100644
index 0000000..2083859
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Keyden.1Password.Sdk.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0
+ Keyden.OnePassword.SDK
+ True
+
+
+
+
+
+
+
diff --git a/src/Keyden.1Password.Sdk/OnePassSdk.cs b/src/Keyden.1Password.Sdk/OnePassSdk.cs
new file mode 100644
index 0000000..06376e3
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/OnePassSdk.cs
@@ -0,0 +1,379 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Keyden.OnePassword.SDK.Types;
+
+namespace Keyden.OnePassword.SDK;
+
+static file class TaskRunMarshaller
+{
+ public static YieldTask Yield()
+ {
+ return new();
+ }
+
+ public struct YieldTask
+ {
+ public YieldAwaiter GetAwaiter()
+ {
+ return new();
+ }
+ }
+
+ public struct YieldAwaiter : ICriticalNotifyCompletion
+ {
+ public bool IsCompleted => false;
+ public void GetResult() { }
+
+ private static ContextCallback? Runner { get; set; }
+ public void OnCompleted(Action continuation)
+ {
+ [DebuggerNonUserCode]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static void executionContextRunner(object obj) => (obj as Action)!();
+ Runner ??= executionContextRunner!;
+
+ var ctx = ExecutionContext.Capture();
+ if (ctx is not null)
+ Task.Run(() => ExecutionContext.Run(ctx, Runner, continuation));
+ else
+ Task.Run(() => continuation());
+
+ }
+
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ Task.Run(() => continuation());
+ }
+ }
+}
+
+public partial class OnePassSdk : IDisposable, IAsyncDisposable
+{
+ private OnePassSdkConfig Config;
+ public string? AccountName { get; set; }
+
+ private record struct ConnectionInformation(int ClientId, string AccountName);
+ private ConnectionInformation? ConnectionInfo { get; set; }
+
+ public OnePassSdk(OnePassSdkConfig config)
+ {
+ Config = config;
+ }
+
+ private readonly object ConnectLock = new();
+ private void Connect()
+ {
+ lock (ConnectLock)
+ {
+ if (AccountName is null)
+ throw new InvalidOperationException("Must assign an account name prior to use");
+
+ var accountName = AccountName;
+ var configJson = OnePassSdkConfig.ToJson(Config);
+ var initResponse = OnePassSdkInterop.CallSharedLibrary(new()
+ {
+ Kind = OnePassSdkInterop.PayloadKind.InitClient,
+ AccountName = accountName,
+ Payload = configJson,
+ });
+
+ var clientId = int.Parse(initResponse.Payload, CultureInfo.InvariantCulture);
+
+ ConnectionInfo = new(clientId, accountName);
+ }
+ }
+
+ private void Disconnect()
+ {
+ lock (ConnectLock)
+ {
+ if (!ConnectionInfo.HasValue)
+ return;
+
+ try
+ {
+ _ = OnePassSdkInterop.CallSharedLibrary(new()
+ {
+ Kind = OnePassSdkInterop.PayloadKind.ReleaseClient,
+ AccountName = AccountName ?? string.Empty,
+ Payload = ConnectionInfo.Value.ClientId.ToString(null, CultureInfo.InvariantCulture),
+ });
+ }
+ catch { }
+
+ ConnectionInfo = null;
+ }
+ }
+
+ private bool Disposed;
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!Disposed)
+ {
+ Disconnect();
+ Disposed = true;
+ }
+ }
+
+ private string Invoke(string name, [StringSyntax("json")] string parameters)
+ {
+ bool retry;
+ int tryCount = 0;
+ do
+ {
+ retry = false;
+ tryCount++;
+ try
+ {
+ if (!ConnectionInfo.HasValue)
+ Connect();
+ if (!ConnectionInfo.HasValue)
+ throw new OnePassSdkChannelClosedException();
+
+ var connectionInfo = ConnectionInfo.Value;
+ return OnePassSdkInterop.Invoke(connectionInfo.AccountName, new()
+ {
+ Invocation = new()
+ {
+ ClientId = connectionInfo.ClientId,
+ Parameters = new()
+ {
+ Name = name,
+ Parameters = parameters,
+ }
+ },
+ });
+ }
+ catch (OnePassSdkChannelConnectionDroppedException)
+ {
+ Disconnect();
+ retry = true;
+ }
+ catch (OnePassSdkDesktopSessionExpiredException)
+ {
+ Disconnect();
+ retry = true;
+ }
+ } while (retry && tryCount <= 3);
+
+ throw new OnePassSdkChannelClosedException();
+ }
+
+ public string ResolveSecret(string secretReference)
+ {
+ var payload = ResolveSecretPayload.ToJson(new()
+ {
+ SecretReference = secretReference,
+ });
+
+ var response = Invoke("SecretsResolve", payload);
+ return JsonSerializer.Deserialize(response, OnePassTypesJsonContext.Default.String)
+ ?? throw new JsonException($"Failed to parse params: {response}");
+ }
+
+ public async Task ResolveSecretAsync(string secretReference)
+ {
+ await TaskRunMarshaller.Yield();
+ return ResolveSecret(secretReference);
+ }
+
+ public ResolveSecretsResponse ResolveSecrets(IReadOnlyList secretReferences)
+ {
+ var payload = ResolveSecretsPayload.ToJson(new()
+ {
+ SecretReferences = secretReferences,
+ });
+ var response = Invoke("SecretsResolveAll", payload);
+ return ResolveSecretsResponse.FromJson(response);
+ }
+
+ public async Task ResolveSecretsAsync(IReadOnlyList secretReferences)
+ {
+ await TaskRunMarshaller.Yield();
+ return ResolveSecrets(secretReferences);
+ }
+
+ public void ValidateSecretReference(string secretReference)
+ {
+ var payload = ValidateSecretReferencePayload.ToJson(new()
+ {
+ SecretReference = secretReference,
+ });
+
+ _ = Invoke("SecretsResolve", payload);
+ }
+
+ public async Task ValidateSecretReferenceAsync(string secretReference)
+ {
+ await TaskRunMarshaller.Yield();
+ ValidateSecretReference(secretReference);
+ }
+
+ public GeneratePasswordResponse GeneratePassword(PasswordRecipe recipe)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IReadOnlyList GetAllVaults()
+ {
+ var res = Invoke("VaultsList", @"{""params"": null}");
+ return JsonSerializer.Deserialize(res, OnePassTypesJsonContext.Default.IReadOnlyListGetVaultResponse) ?? [];
+ }
+
+ public async Task> GetAllVaultsAsync()
+ {
+ await TaskRunMarshaller.Yield();
+ return GetAllVaults();
+ }
+
+ public Item UpdateItem(Item item)
+ {
+ var res = Invoke("ItemsPut", ItemsPutPayload.ToJson(new()
+ {
+ Item = item,
+ }));
+ return Item.FromJson(res);
+ }
+
+ public async Task- UpdateItemAsync(Item item)
+ {
+ await TaskRunMarshaller.Yield();
+ return UpdateItem(item);
+ }
+
+ public void DeleteItem(string vaultId, string itemId)
+ {
+ _ = Invoke("ItemsDelete", DeleteItemPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ ItemId = itemId,
+ }));
+ }
+
+ public async Task DeleteItemAsync(string vaultId, string itemId)
+ {
+ await TaskRunMarshaller.Yield();
+ DeleteItem(vaultId, itemId);
+ }
+
+ public ItemsDeleteAllResponse DeleteItems(string vaultId, IReadOnlyList itemIds)
+ {
+ var res = Invoke("ItemsDeleteAll", DeleteAllItemsPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ ItemIds = itemIds,
+ }));
+ return ItemsDeleteAllResponse.FromJson(res);
+ }
+
+ public async Task DeleteItemsAsync(string vaultId, IReadOnlyList itemIds)
+ {
+ await TaskRunMarshaller.Yield();
+ return DeleteItems(vaultId, itemIds);
+ }
+
+ public Item CreateItem(ItemCreateParams itemParam)
+ {
+ var res = Invoke("ItemsCreate", ItemsCreatePayload.ToJson(new() { Params = itemParam }));
+ return Item.FromJson(res);
+ }
+
+ public async Task
- CreateItemAsync(ItemCreateParams itemParam)
+ {
+ await TaskRunMarshaller.Yield();
+ return CreateItem(itemParam);
+ }
+
+ public ItemsUpdateAllResponse CreateItems(string vaultId, IReadOnlyList itemsParam)
+ {
+ var res = Invoke("ItemsCreateAll", ItemsCreateAllPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ Params = itemsParam,
+ }));
+ return ItemsUpdateAllResponse.FromJson(res);
+ }
+
+ public async Task CreateItemsAsync(string vaultId, IReadOnlyList itemsParam)
+ {
+ await TaskRunMarshaller.Yield();
+ return CreateItems(vaultId, itemsParam);
+ }
+
+ // https://github.com/1Password/onepassword-sdk-python/blob/main/src/onepassword/items.py#L91
+ public Item GetItem(string vaultId, string itemId)
+ {
+ var res = Invoke("ItemsGet", GetItemsPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ ItemId = itemId,
+ }));
+ return Item.FromJson(res);
+ }
+
+ public async Task
- GetItemAsync(string vaultId, string itemId)
+ {
+ await TaskRunMarshaller.Yield();
+ return GetItem(vaultId, itemId);
+ }
+
+ public ItemsGetAllResponse GetItems(string vaultId, IReadOnlyList itemIds)
+ {
+ var res = Invoke("ItemsGetAll", GetAllItemsPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ ItemIds = itemIds,
+ }));
+ return ItemsGetAllResponse.FromJson(res);
+ }
+
+ public async Task GetItemsAsync(string vaultId, IReadOnlyList itemIds)
+ {
+ await TaskRunMarshaller.Yield();
+ return GetItems(vaultId, itemIds);
+ }
+
+ public IReadOnlyList ListItems(string vaultId, IReadOnlyList filters)
+ {
+ var res = Invoke("ItemsList", ListItemsPayload.ToJson(new()
+ {
+ VaultId = vaultId,
+ Filters = filters,
+ }));
+ return JsonSerializer.Deserialize(res, OnePassTypesJsonContext.Default.IReadOnlyListItemOverview)
+ ?? [];
+ }
+
+ public async Task> ListItemsAsync(string vaultId, IReadOnlyList filters)
+ {
+ await TaskRunMarshaller.Yield();
+ return ListItems(vaultId, filters);
+ }
+
+ ~OnePassSdk()
+ {
+ Dispose(disposing: false);
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. UpdateItem cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ async ValueTask IAsyncDisposable.DisposeAsync()
+ {
+ await TaskRunMarshaller.Yield();
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/OnePassSdkExceptions.cs b/src/Keyden.1Password.Sdk/OnePassSdkExceptions.cs
new file mode 100644
index 0000000..a719188
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/OnePassSdkExceptions.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Keyden.OnePassword.SDK;
+
+public class OnePassSdkException : Exception
+{
+ public OnePassSdkException() {}
+ public OnePassSdkException(string? message) : base(message) { }
+ public OnePassSdkException(string? message, Exception? innerException) : base(message, innerException) { }
+}
+
+public class OnePassSdkChannelClosedException() : OnePassSdkException("Desktop app connection channel is closed.") { }
+public class OnePassSdkChannelConnectionDroppedException() : OnePassSdkException("Connection was unexpectedly dropped by the desktop app.") { }
+public class OnePassSdkInternalException(int returnCode) : OnePassSdkException($"An internal error occurred. Please contact 1Password support and mention the return returnCode: {returnCode}") { }
+public class OnePassSdkDesktopSessionExpiredException(string message) : OnePassSdkException(message) { }
+public class OnePassSdkRateLimitExceededException(string message) : OnePassSdkException(message) { }
diff --git a/src/Keyden.1Password.Sdk/OnePassSdkInterop.cs b/src/Keyden.1Password.Sdk/OnePassSdkInterop.cs
new file mode 100644
index 0000000..fb2e4b9
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/OnePassSdkInterop.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+using Keyden.OnePassword.SDK.Types;
+
+namespace Keyden.OnePassword.SDK;
+
+internal unsafe static partial class OnePassSdkInterop
+{
+ [JsonSerializable(typeof(PayloadMessage))]
+ [JsonSerializable(typeof(PayloadResponse))]
+ [JsonSerializable(typeof(PayloadTypedError))]
+ [JsonSerializable(typeof(InvocationPayload))]
+ internal partial class InteropJsonContext : JsonSerializerContext
+ {
+ }
+
+ [JsonConverter(typeof(PayloadKindJsonConverter))]
+ public enum PayloadKind
+ {
+ None,
+ InitClient,
+ Invoke,
+ ReleaseClient,
+ }
+
+ private sealed class PayloadKindJsonConverter : JsonConverter
+ {
+ public override PayloadKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException($"Unexpected token parsing {nameof(AutofillBehavior)}. Expected String, got {reader.TokenType}.");
+
+ return reader.GetString() switch
+ {
+ "none" => PayloadKind.None,
+ "init_client" => PayloadKind.InitClient,
+ "invoke" => PayloadKind.Invoke,
+ "release_client" => PayloadKind.ReleaseClient,
+ _ => throw new NotImplementedException(),
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, PayloadKind value, JsonSerializerOptions options)
+ {
+ var valueStr = value switch
+ {
+ PayloadKind.None => "none",
+ PayloadKind.InitClient => "init_client",
+ PayloadKind.Invoke => "invoke",
+ PayloadKind.ReleaseClient => "release_client",
+ _ => throw new NotImplementedException(),
+ };
+ writer.WriteStringValue(valueStr);
+ }
+ }
+
+ public record struct PayloadMessage()
+ {
+ [JsonPropertyName("kind")]
+ public PayloadKind Kind { get; set; } = PayloadKind.None;
+
+ [JsonPropertyName("account_name")]
+ public required string AccountName { get; set; }
+
+ [JsonPropertyName("payload")]
+ [JsonConverter(typeof(Base64JsonConverter))]
+ public required string Payload { get; set; }
+ }
+
+ public record struct PayloadResponse()
+ {
+ [JsonPropertyName("success")]
+ public required bool Success { get; set; } = false;
+
+ [JsonPropertyName("payload")]
+ [JsonConverter(typeof(ByteArrayJsonConverter))]
+ public required string Payload { get; set; }
+ }
+
+ public record struct PayloadTypedError
+ {
+ public PayloadTypedError() { }
+ [JsonPropertyName("name")]
+ public string ErrorName { get; set; } = string.Empty;
+ [JsonPropertyName("message")]
+ public string Message { get; set; } = string.Empty;
+ }
+
+ [DoesNotReturn]
+ public static void ThrowTypedException(string payload)
+ {
+ PayloadTypedError typedError;
+ try
+ {
+ typedError = JsonSerializer.Deserialize(payload, InteropJsonContext.Default.PayloadTypedError);
+ }
+ catch
+ {
+ throw new OnePassSdkException(payload);
+ }
+
+ throw typedError switch
+ {
+ { ErrorName: "DesktopSessionExpired" } => new OnePassSdkDesktopSessionExpiredException(payload),
+ { ErrorName: "RateLimitExceeded" } => new OnePassSdkRateLimitExceededException(payload),
+ { Message: not "" or null } => new OnePassSdkException(typedError.Message),
+ _ => new OnePassSdkException(payload),
+ };
+ }
+
+ public static PayloadResponse CallSharedLibrary(PayloadMessage payload)
+ {
+ var json = JsonSerializer.Serialize(payload, InteropJsonContext.Default.PayloadMessage);
+ var returnCode = OnePassSdkNative.SendMessage(json, out var responseStr);
+
+ if (returnCode != 0)
+ {
+ bool isOsx = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
+ throw returnCode switch
+ {
+ -3 when isOsx => new OnePassSdkChannelClosedException(),
+ -7 when isOsx => new OnePassSdkChannelConnectionDroppedException(),
+ -2 when !isOsx => new OnePassSdkChannelClosedException(),
+ -5 when !isOsx => new OnePassSdkChannelConnectionDroppedException(),
+ _ => new OnePassSdkInternalException(returnCode),
+ };
+ }
+
+ var response = JsonSerializer.Deserialize(responseStr, InteropJsonContext.Default.PayloadResponse);
+ if (!response.Success)
+ ThrowTypedException(response.Payload);
+
+ return response;
+ }
+
+ public static string Invoke(string accountName, InvocationPayload payload)
+ {
+ var json = JsonSerializer.Serialize(payload, InteropJsonContext.Default.InvocationPayload);
+
+ var ret = CallSharedLibrary(new()
+ {
+ Kind = PayloadKind.Invoke,
+ AccountName = accountName,
+ Payload = json,
+ });
+
+ return ret.Payload;
+ }
+
+ public record struct InvocationPayload()
+ {
+ [JsonPropertyName("invocation")]
+ public required InvocationObject Invocation { get; set; }
+
+ public record struct InvocationObject()
+ {
+ [JsonPropertyName("clientId")]
+ public required int ClientId { get; set; }
+
+ [JsonPropertyName("parameters")]
+ public required ParametersObject Parameters { get; set; }
+
+ public record struct ParametersObject()
+ {
+ [JsonPropertyName("name")]
+ public required string Name { get; set; } = "";
+
+ [JsonPropertyName("parameters")]
+ [JsonConverter(typeof(RawJsonConverter))]
+ public required string Parameters { get; set; }
+ }
+ }
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/OnePassSdkNative.cs b/src/Keyden.1Password.Sdk/OnePassSdkNative.cs
new file mode 100644
index 0000000..0cac37e
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/OnePassSdkNative.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Keyden.OnePassword.SDK;
+
+// using the python SDK as a reference
+// https://github.com/1Password/onepassword-sdk-python/blob/main/src/onepassword/desktop_core.py
+
+internal unsafe static partial class OnePassSdkNative
+{
+ public const string LibSdkIpcClient = "op_sdk_ipc_client";
+
+ [LibraryImport(LibSdkIpcClient, EntryPoint = "op_sdk_ipc_send_message", StringMarshalling = StringMarshalling.Utf8)]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial int SendMessage(byte* msg, nuint msgLen, out byte* response, out nuint responseLen, out nuint responseCap);
+
+ [LibraryImport(LibSdkIpcClient, EntryPoint = "op_sdk_ipc_free_response", StringMarshalling = StringMarshalling.Utf8)]
+ [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
+ public static partial void FreeResponse(byte* msg, nuint length, nuint capacity);
+
+ internal static readonly UTF8Encoding Utf8 = new(false);
+ public static int SendMessage(string message, out string response)
+ {
+ var messageUtf8 = Utf8.GetBytes(message);
+ var messageUtf8Len = (nuint)messageUtf8.Length;
+
+ byte* responsePtr = null;
+ nuint responseLen = nuint.Zero;
+ nuint responseCap = nuint.Zero;
+ int retCode;
+
+ fixed (byte* messageUtf8Ptr = messageUtf8)
+ {
+ retCode = SendMessage(messageUtf8Ptr, messageUtf8Len, out responsePtr, out responseLen, out responseCap);
+ }
+
+ if ((nuint)responsePtr == nuint.Zero)
+ {
+ response = string.Empty;
+ }
+ else
+ {
+ response = Utf8.GetString(responsePtr, checked((int)responseLen));
+ FreeResponse(responsePtr, responseLen, responseCap);
+ }
+
+ return retCode;
+ }
+
+ static OnePassSdkNative()
+ {
+ NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver);
+ }
+
+ private static nint DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
+ {
+ if (libraryName != LibSdkIpcClient)
+ return nint.Zero;
+
+ string[] paths = [];
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ paths = [
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "1password/op_sdk_ipc_client.dll"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "1password/app/8/op_sdk_ipc_client.dll"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "1password/app/8/op_sdk_ipc_client.dll"),
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "1password/app/8/op_sdk_ipc_client.dll"),
+ ];
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ paths = [
+ Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Applications/1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"),
+ Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "1Password.app/Contents/Frameworks/libop_sdk_ipc_client.dylib"),
+ ];
+ }
+ else // assume linux/unix
+ {
+ paths = [
+ "/usr/bin/1password/libop_sdk_ipc_client.so",
+ "/opt/1Password/libop_sdk_ipc_client.so",
+ "/snap/bin/1password/libop_sdk_ipc_client.so",
+ ];
+ }
+
+ foreach (var path in paths)
+ {
+ if (File.Exists(path))
+ return NativeLibrary.Load(path, assembly, null);
+ }
+
+ return nint.Zero;
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/RawJsonConverter.cs b/src/Keyden.1Password.Sdk/RawJsonConverter.cs
new file mode 100644
index 0000000..1a53180
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/RawJsonConverter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK;
+
+internal class RawJsonConverter : JsonConverter
+{
+ public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ using var doc = JsonDocument.ParseValue(ref reader);
+ return doc.RootElement.GetRawText();
+ }
+
+ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
+ {
+ writer.WriteRawValue(value, true);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/AutofillBehavior.cs b/src/Keyden.1Password.Sdk/Types/AutofillBehavior.cs
new file mode 100644
index 0000000..7f06b7d
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/AutofillBehavior.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+[JsonConverter(typeof(AutofillBehaviorJsonConverter))]
+public enum AutofillBehavior
+{
+ AnywhereOnWebsite,
+ ExactDomain,
+ Never,
+ Unknown,
+}
+
+public sealed class AutofillBehaviorJsonConverter : JsonConverter
+{
+ public override AutofillBehavior Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException($"Unexpected token parsing {nameof(AutofillBehavior)}. Expected String, got {reader.TokenType}.");
+
+ return reader.GetString() switch
+ {
+ "AnywhereOnWebsite" => AutofillBehavior.AnywhereOnWebsite,
+ "ExactDomain" => AutofillBehavior.ExactDomain,
+ "Never" => AutofillBehavior.Never,
+ _ => AutofillBehavior.Unknown,
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, AutofillBehavior value, JsonSerializerOptions options)
+ {
+ var valueStr = value switch
+ {
+ AutofillBehavior.AnywhereOnWebsite => "AnywhereOnWebsite",
+ AutofillBehavior.ExactDomain => "ExactDomain",
+ AutofillBehavior.Never => "Never",
+ _ => throw new NotImplementedException(),
+ };
+ writer.WriteStringValue(valueStr);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/DocumentCreateParams.cs b/src/Keyden.1Password.Sdk/Types/DocumentCreateParams.cs
new file mode 100644
index 0000000..cdb4407
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/DocumentCreateParams.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct DocumentCreateParams()
+{
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("content")]
+ public required byte[] Content { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/FileAttributes.cs b/src/Keyden.1Password.Sdk/Types/FileAttributes.cs
new file mode 100644
index 0000000..1abf7f7
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/FileAttributes.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct FileAttributes()
+{
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("size")]
+ public required int Size { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/FileCreateParams.cs b/src/Keyden.1Password.Sdk/Types/FileCreateParams.cs
new file mode 100644
index 0000000..8d9c93b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/FileCreateParams.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct FileCreateParams()
+{
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("content")]
+ public required byte[] Content { get; set; }
+
+ [JsonPropertyName("sectionId")]
+ public required string SectionId { get; set; }
+
+ [JsonPropertyName("fieldId")]
+ public required string FieldId { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/GeneratePasswordResponse.cs b/src/Keyden.1Password.Sdk/Types/GeneratePasswordResponse.cs
new file mode 100644
index 0000000..74a486b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/GeneratePasswordResponse.cs
@@ -0,0 +1,10 @@
+using System.Text.Json;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct GeneratePasswordResponse()
+{
+
+ internal static string ToJson(GeneratePasswordResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.GeneratePasswordResponse);
+ internal static GeneratePasswordResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.GeneratePasswordResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/GetVaultResponse.cs b/src/Keyden.1Password.Sdk/Types/GetVaultResponse.cs
new file mode 100644
index 0000000..1ad80e4
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/GetVaultResponse.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct GetVaultResponse()
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; set; }
+
+ [JsonPropertyName("description")]
+ public required string Description { get; set; }
+
+ [JsonPropertyName("vaultType")]
+ public required string VaultType { get; set; }
+
+ [JsonPropertyName("activeItemCount")]
+ public required int ActiveItemCount { get; set; }
+
+ [JsonPropertyName("contentVersion")]
+ public required int ContentVersion { get; set; }
+
+ [JsonPropertyName("attributeVersion")]
+ public required int AttributeVersion { get; set; }
+
+ [JsonPropertyName("createdAt")]
+ public required DateTime CreatedAt { get; set; }
+
+ [JsonPropertyName("updatedAt")]
+ public required DateTime UpdatedAt { get; set; }
+
+ internal static string ToJson(GetVaultResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.GetVaultResponse);
+ internal static GetVaultResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.GetVaultResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/Item.cs b/src/Keyden.1Password.Sdk/Types/Item.cs
new file mode 100644
index 0000000..1a59cc9
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/Item.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct Item()
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; set; }
+
+ [JsonPropertyName("category")]
+ [JsonConverter(typeof(ItemCategoryJsonConverter))]
+ public required ItemCategory Category { get; set; }
+
+ [JsonPropertyName("vaultId")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("fields")]
+ public required IReadOnlyList Fields { get; set; }
+
+ [JsonPropertyName("sections")]
+ public required IReadOnlyList Sections { get; set; }
+
+ [JsonPropertyName("notes")]
+ public required string Notes { get; set; }
+
+ [JsonPropertyName("tags")]
+ public required IReadOnlyList Tags { get; set; }
+
+ [JsonPropertyName("websites")]
+ public required IReadOnlyList Websites { get; set; }
+
+ [JsonPropertyName("version")]
+ public required int Version { get; set; }
+
+ [JsonPropertyName("files")]
+ public required IReadOnlyList Files { get; set; }
+
+ [JsonPropertyName("document")]
+ public FileAttributes? Document { get; set; }
+
+ [JsonPropertyName("createdAt")]
+ public DateTime CreatedAt { get; set; }
+
+ [JsonPropertyName("updatedAt")]
+ public DateTime UpdatedAt { get; set; }
+
+ public static string ToJson(Item value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.Item);
+ public static Item FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.Item);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemCategory.cs b/src/Keyden.1Password.Sdk/Types/ItemCategory.cs
new file mode 100644
index 0000000..3896364
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemCategory.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+[JsonConverter(typeof(ItemCategoryJsonConverter))]
+public enum ItemCategory
+{
+ Login,
+ SecureNote,
+ CreditCard,
+ CryptoWallet,
+ Identity,
+ Password,
+ Document,
+ ApiCredentials,
+ BankAccount,
+ Database,
+ DriverLicense,
+ Email,
+ MedicalRecord,
+ Membership,
+ OutdoorLicense,
+ Passport,
+ Rewards,
+ Router,
+ Server,
+ SshKey,
+ SocialSecurityNumber,
+ SoftwareLicense,
+ Person,
+ Unsupported,
+ Unknown,
+}
+
+public sealed class ItemCategoryJsonConverter : JsonConverter
+{
+ public override ItemCategory Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException($"Unexpected token parsing {nameof(ItemCategory)}. Expected String, got {reader.TokenType}.");
+
+ return reader.GetString() switch
+ {
+ "Login" => ItemCategory.Login,
+ "SecureNote" => ItemCategory.SecureNote,
+ "CreditCard" => ItemCategory.CreditCard,
+ "CryptoWallet" => ItemCategory.CryptoWallet,
+ "Identity" => ItemCategory.Identity,
+ "Password" => ItemCategory.Password,
+ "Document" => ItemCategory.Document,
+ "ApiCredentials" => ItemCategory.ApiCredentials,
+ "BankAccount" => ItemCategory.BankAccount,
+ "Database" => ItemCategory.Database,
+ "DriverLicense" => ItemCategory.DriverLicense,
+ "Email" => ItemCategory.Email,
+ "MedicalRecord" => ItemCategory.MedicalRecord,
+ "Membership" => ItemCategory.Membership,
+ "OutdoorLicense" => ItemCategory.OutdoorLicense,
+ "Passport" => ItemCategory.Passport,
+ "Rewards" => ItemCategory.Rewards,
+ "Router" => ItemCategory.Router,
+ "Server" => ItemCategory.Server,
+ "SshKey" => ItemCategory.SshKey,
+ "SocialSecurityNumber" => ItemCategory.SocialSecurityNumber,
+ "SoftwareLicense" => ItemCategory.SoftwareLicense,
+ "Person" => ItemCategory.Person,
+ "Unsupported" => ItemCategory.Unsupported,
+ _ => ItemCategory.Unknown,
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, ItemCategory value, JsonSerializerOptions options)
+ {
+ var valueStr = value switch
+ {
+ ItemCategory.Login => "Login",
+ ItemCategory.SecureNote => "SecureNote",
+ ItemCategory.CreditCard => "CreditCard",
+ ItemCategory.CryptoWallet => "CryptoWallet",
+ ItemCategory.Identity => "Identity",
+ ItemCategory.Password => "Password",
+ ItemCategory.Document => "Document",
+ ItemCategory.ApiCredentials => "ApiCredentials",
+ ItemCategory.BankAccount => "BankAccount",
+ ItemCategory.Database => "Database",
+ ItemCategory.DriverLicense => "DriverLicense",
+ ItemCategory.Email => "Email",
+ ItemCategory.MedicalRecord => "MedicalRecord",
+ ItemCategory.Membership => "Membership",
+ ItemCategory.OutdoorLicense => "OutdoorLicense",
+ ItemCategory.Passport => "Passport",
+ ItemCategory.Rewards => "Rewards",
+ ItemCategory.Router => "Router",
+ ItemCategory.Server => "Server",
+ ItemCategory.SshKey => "SshKey",
+ ItemCategory.SocialSecurityNumber => "SocialSecurityNumber",
+ ItemCategory.SoftwareLicense => "SoftwareLicense",
+ ItemCategory.Person => "Person",
+ ItemCategory.Unsupported => "Unsupported",
+ _ => throw new NotImplementedException(),
+ };
+ writer.WriteStringValue(valueStr);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemCreateParams.cs b/src/Keyden.1Password.Sdk/Types/ItemCreateParams.cs
new file mode 100644
index 0000000..1a047ea
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemCreateParams.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemCreateParams()
+{
+ [JsonPropertyName("category")]
+ public required ItemCategory Category { get; set; }
+
+ [JsonPropertyName("vaultId")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; set; }
+
+ [JsonPropertyName("fields")]
+ public IReadOnlyList? Fields { get; set; } = null;
+
+ [JsonPropertyName("sections")]
+ public IReadOnlyList? Sections { get; set; } = null;
+
+ [JsonPropertyName("notes")]
+ public string? Notes { get; set; } = null;
+
+ [JsonPropertyName("tags")]
+ public IReadOnlyList? Tags { get; set; } = null;
+
+ [JsonPropertyName("websites")]
+ public IReadOnlyList? Websites { get; set; } = null;
+
+ [JsonPropertyName("files")]
+ public IReadOnlyList? Files { get; set; } = null;
+
+ [JsonPropertyName("document")]
+ public DocumentCreateParams? Document { get; set; } = null;
+
+ public static string ToJson(ItemCreateParams value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemCreateParams);
+ public static ItemCreateParams FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemCreateParams);
+
+ public static string ToJson(IReadOnlyList value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.IReadOnlyListItemCreateParams);
+
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemErrorResponse.cs b/src/Keyden.1Password.Sdk/Types/ItemErrorResponse.cs
new file mode 100644
index 0000000..4d7b45b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemErrorResponse.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemErrorResponse
+{
+ [JsonPropertyName("type")]
+ public string Type { get; set; } // internal/itemNotFound
+ [JsonPropertyName("message")]
+ public string? Message { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemField.cs b/src/Keyden.1Password.Sdk/Types/ItemField.cs
new file mode 100644
index 0000000..bce6388
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemField.cs
@@ -0,0 +1,25 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemField()
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; set; }
+
+ [JsonPropertyName("sectionId")]
+ public string? SectionId { get; set; }
+
+ [JsonPropertyName("fieldType")]
+ [JsonConverter(typeof(ItemFieldTypeJsonConverter))]
+ public required ItemFieldType FieldType { get; set; }
+
+ [JsonPropertyName("value")]
+ public required string Value { get; set; }
+
+ [JsonPropertyName("details")]
+ public ItemFieldDetails? Details { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemFieldDetails.cs b/src/Keyden.1Password.Sdk/Types/ItemFieldDetails.cs
new file mode 100644
index 0000000..cc8730b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemFieldDetails.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemFieldDetails()
+{
+ [JsonPropertyName("type")]
+ public required string Type { get; set; } // Otp/SshKey/Address
+
+ [JsonPropertyName("content")]
+ [JsonConverter(typeof(RawJsonConverter))]
+ public required string Content { get; set; } // https://github.com/1Password/onepassword-sdk-python/blob/main/src/onepassword/types.py#L335
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemFieldType.cs b/src/Keyden.1Password.Sdk/Types/ItemFieldType.cs
new file mode 100644
index 0000000..9d0e7f5
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemFieldType.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public enum ItemFieldType
+{
+ Text,
+ Concealed,
+ CreditCardType,
+ CreditCardNumber,
+ Phone,
+ Url,
+ Totp,
+ Email,
+ Reference,
+ SshKey,
+ Menu,
+ MonthYear,
+ Address,
+ Date,
+ Unsupported,
+ Unknown,
+}
+
+public sealed class ItemFieldTypeJsonConverter : JsonConverter
+{
+ public override ItemFieldType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException($"Unexpected token parsing {nameof(ItemFieldType)}. Expected String, got {reader.TokenType}.");
+
+ return reader.GetString() switch
+ {
+ "Text" => ItemFieldType.Text,
+ "Concealed" => ItemFieldType.Concealed,
+ "CreditCardType" => ItemFieldType.CreditCardType,
+ "CreditCardNumber" => ItemFieldType.CreditCardNumber,
+ "Phone" => ItemFieldType.Phone,
+ "Url" => ItemFieldType.Url,
+ "Totp" => ItemFieldType.Totp,
+ "Email" => ItemFieldType.Email,
+ "Reference" => ItemFieldType.Reference,
+ "SshKey" => ItemFieldType.SshKey,
+ "Menu" => ItemFieldType.Menu,
+ "MonthYear" => ItemFieldType.MonthYear,
+ "Address" => ItemFieldType.Address,
+ "Date" => ItemFieldType.Date,
+ "Unsupported" => ItemFieldType.Unsupported,
+ _ => ItemFieldType.Unknown,
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, ItemFieldType value, JsonSerializerOptions options)
+ {
+ var valueStr = value switch
+ {
+ ItemFieldType.Text => "Text",
+ ItemFieldType.Concealed => "Concealed",
+ ItemFieldType.CreditCardType => "CreditCardType",
+ ItemFieldType.CreditCardNumber => "CreditCardNumber",
+ ItemFieldType.Phone => "Phone",
+ ItemFieldType.Url => "Url",
+ ItemFieldType.Totp => "Totp",
+ ItemFieldType.Email => "Email",
+ ItemFieldType.Reference => "Reference",
+ ItemFieldType.SshKey => "SshKey",
+ ItemFieldType.Menu => "Menu",
+ ItemFieldType.MonthYear => "MonthYear",
+ ItemFieldType.Address => "Address",
+ ItemFieldType.Date => "Date",
+ ItemFieldType.Unsupported => "Unsupported",
+ _ => throw new NotImplementedException(),
+ };
+ writer.WriteStringValue(valueStr);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemFile.cs b/src/Keyden.1Password.Sdk/Types/ItemFile.cs
new file mode 100644
index 0000000..5f6b1f3
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemFile.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemFile()
+{
+ [JsonPropertyName("attributes")]
+ public required FileAttributes Attributes { get; set; }
+
+ [JsonPropertyName("sectionId")]
+ public required string SectionId { get; set; }
+
+ [JsonPropertyName("fieldId")]
+ public required string FieldId { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemOverview.cs b/src/Keyden.1Password.Sdk/Types/ItemOverview.cs
new file mode 100644
index 0000000..89957ea
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemOverview.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemOverview()
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; set; }
+
+ [JsonPropertyName("category")]
+ [JsonConverter(typeof(ItemCategoryJsonConverter))]
+ public required ItemCategory Category { get; set; }
+
+ [JsonPropertyName("vaultId")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("websites")]
+ public required IReadOnlyList Websites { get; set; }
+
+ [JsonPropertyName("tags")]
+ public required IReadOnlyList Tags { get; set; }
+
+ [JsonPropertyName("createdAt")]
+ public required DateTime CreatedAt { get; set; }
+
+ [JsonPropertyName("updatedAt")]
+ public required DateTime UpdatedAt { get; set; }
+
+ [JsonPropertyName("state")]
+ [JsonConverter(typeof(ItemStateJsonConverter))]
+ public required ItemState State { get; set; }
+}
+
+public struct ItemListFilter()
+{
+ // TODO:
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemSection.cs b/src/Keyden.1Password.Sdk/Types/ItemSection.cs
new file mode 100644
index 0000000..7b585a8
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemSection.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemSection()
+{
+ [JsonPropertyName("id")]
+ public required string Id { get; set; }
+
+ [JsonPropertyName("title")]
+ public required string Title {get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemState.cs b/src/Keyden.1Password.Sdk/Types/ItemState.cs
new file mode 100644
index 0000000..00a3549
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemState.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+[JsonConverter(typeof(ItemStateJsonConverter))]
+public enum ItemState
+{
+ Active,
+ Archived,
+ Unknown,
+}
+
+public sealed class ItemStateJsonConverter : JsonConverter
+{
+ public override ItemState Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType != JsonTokenType.String)
+ throw new JsonException($"Unexpected token parsing {nameof(ItemState)}. Expected String, got {reader.TokenType}.");
+
+ return reader.GetString() switch
+ {
+ "active" => ItemState.Active,
+ "archived" => ItemState.Archived,
+ _ => ItemState.Unknown,
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, ItemState value, JsonSerializerOptions options)
+ {
+ var valueStr = value switch
+ {
+ ItemState.Active => "active",
+ ItemState.Archived => "archived",
+ _ => throw new NotImplementedException(),
+ };
+ writer.WriteStringValue(valueStr);
+ }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemUpdateFailureReason.cs b/src/Keyden.1Password.Sdk/Types/ItemUpdateFailureReason.cs
new file mode 100644
index 0000000..b19cf67
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemUpdateFailureReason.cs
@@ -0,0 +1,5 @@
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct ItemUpdateFailureReason()
+{
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemsDeleteAllResponse.cs b/src/Keyden.1Password.Sdk/Types/ItemsDeleteAllResponse.cs
new file mode 100644
index 0000000..602bddf
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemsDeleteAllResponse.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemsDeleteAllResponse()
+{
+ [JsonPropertyName("individualResponses")]
+ public required IReadOnlyList> IndividualResponses { get; set; }
+
+ public static string ToJson(ItemsDeleteAllResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsDeleteAllResponse);
+ public static ItemsDeleteAllResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsDeleteAllResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemsGetAllResponse.cs b/src/Keyden.1Password.Sdk/Types/ItemsGetAllResponse.cs
new file mode 100644
index 0000000..37ca9ba
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemsGetAllResponse.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemsGetAllResponse()
+{
+ [JsonPropertyName("individualResponses")]
+ public required IReadOnlyList> IndividualResponses { get; set; }
+
+ public static string ToJson(ItemsGetAllResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsGetAllResponse);
+ public static ItemsGetAllResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsGetAllResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ItemsUpdateAllResponse.cs b/src/Keyden.1Password.Sdk/Types/ItemsUpdateAllResponse.cs
new file mode 100644
index 0000000..d5de706
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ItemsUpdateAllResponse.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct ItemsUpdateAllResponse()
+{
+ [JsonPropertyName("individualResponses")]
+ public required IReadOnlyList> IndividualResponses { get; set; }
+
+ public static string ToJson(ItemsUpdateAllResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsUpdateAllResponse);
+ public static ItemsUpdateAllResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsUpdateAllResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/None.cs b/src/Keyden.1Password.Sdk/Types/None.cs
new file mode 100644
index 0000000..3b1f875
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/None.cs
@@ -0,0 +1,4 @@
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct None()
+{}
diff --git a/src/Keyden.1Password.Sdk/Types/OnePassSdkConfig.cs b/src/Keyden.1Password.Sdk/Types/OnePassSdkConfig.cs
new file mode 100644
index 0000000..373f36b
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/OnePassSdkConfig.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Runtime.InteropServices;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+using CpuArchitecture = Architecture;
+
+public record class OnePassSdkConfig
+{
+ [JsonPropertyName("programmingLanguage")]
+ public string ProgrammingLanguage { get; init; } = "C# (Keyden)";
+
+ [JsonPropertyName("sdkVersion")]
+ public string SdkVersion { get; init; } = "0040003";
+
+ [JsonPropertyName("integrationName")]
+ public string IntegrationName { get; init; } = "Unknown";
+
+ [JsonPropertyName("integrationVersion")]
+ public string IntegrationVersion { get; init; } = "Unknown";
+
+ [JsonPropertyName("requestLibraryName")]
+ public string RequestLibraryName { get; init; } = "none";
+
+ [JsonPropertyName("requestLibraryVersion")]
+ public string RequestLibraryVersion { get; init; } = "0.0.0";
+
+ [JsonPropertyName("os")]
+ public string OS { get; init; } = Environment.OSVersion.Platform switch
+ {
+ PlatformID.Win32S => "Windows",
+ PlatformID.Win32Windows => "Windows",
+ PlatformID.Win32NT => "Windows",
+ PlatformID.WinCE => "Windows",
+ PlatformID.Unix => "Linux/Unix",
+ PlatformID.Xbox => "Xbox",
+ PlatformID.MacOSX => "MacOS",
+ PlatformID.Other => "Unknown",
+ _ => "Unknown",
+ };
+
+ [JsonPropertyName("osVersion")]
+ public string OSVersion { get; init; } = Environment.OSVersion.Version.ToString();
+
+ [JsonPropertyName("architecture")]
+ public string Architecture { get; init; } = RuntimeInformation.ProcessArchitecture switch
+ {
+ CpuArchitecture.X86 => "x86",
+ CpuArchitecture.X64 => "x64",
+ CpuArchitecture.Arm => "arm",
+ CpuArchitecture.Arm64 => "arm64",
+ CpuArchitecture.Wasm => "wasm",
+ CpuArchitecture.S390x => "s390x",
+ CpuArchitecture.LoongArch64 => "loongarch64",
+ CpuArchitecture.Armv6 => "armv6",
+ CpuArchitecture.Ppc64le => "ppc64le",
+ CpuArchitecture.RiscV64 => "riscv64",
+ _ => throw new NotImplementedException(),
+ };
+
+ internal static string ToJson(OnePassSdkConfig value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.OnePassSdkConfig);
+ internal static OnePassSdkConfig? FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.OnePassSdkConfig);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/OnePassTypes.Internal.cs b/src/Keyden.1Password.Sdk/Types/OnePassTypes.Internal.cs
new file mode 100644
index 0000000..3ee0457
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/OnePassTypes.Internal.cs
@@ -0,0 +1,165 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+[JsonSerializable(typeof(OnePassSdkConfig))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(ResolveSecretPayload))]
+[JsonSerializable(typeof(ResolveSecretsPayload))]
+[JsonSerializable(typeof(ResolveSecretsResponse))]
+[JsonSerializable(typeof(ValidateSecretReferencePayload))]
+[JsonSerializable(typeof(PasswordRecipe))]
+[JsonSerializable(typeof(GeneratePasswordResponse))]
+[JsonSerializable(typeof(GetVaultResponse))]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(ItemOverview))]
+[JsonSerializable(typeof(ItemsGetAllResponse))]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(ItemCreateParams))]
+[JsonSerializable(typeof(ItemsUpdateAllResponse))]
+[JsonSerializable(typeof(IReadOnlyList))]
+[JsonSerializable(typeof(ItemsPutPayload))]
+[JsonSerializable(typeof(GetItemPayload))]
+[JsonSerializable(typeof(GetItemsPayload))]
+[JsonSerializable(typeof(GetAllItemsPayload))]
+[JsonSerializable(typeof(ListItemsPayload))]
+[JsonSerializable(typeof(DeleteItemPayload))]
+[JsonSerializable(typeof(DeleteAllItemsPayload))]
+[JsonSerializable(typeof(ItemsDeleteAllResponse))]
+[JsonSerializable(typeof(ItemsCreateAllPayload))]
+[JsonSerializable(typeof(ItemsCreatePayload))]
+internal partial class OnePassTypesJsonContext : JsonSerializerContext
+{
+}
+
+internal record struct ResolveSecretPayload()
+{
+ [JsonPropertyName("secret_reference")]
+ public required string SecretReference { get; set; }
+
+ internal static string ToJson(ResolveSecretPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ResolveSecretPayload);
+ internal static ResolveSecretPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ResolveSecretPayload);
+}
+
+internal record struct ResolveSecretsPayload()
+{
+ [JsonPropertyName("secret_references")]
+ public required IReadOnlyList SecretReferences { get; set; }
+
+ internal static string ToJson(ResolveSecretsPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ResolveSecretsPayload);
+ internal static ResolveSecretsPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ResolveSecretsPayload);
+}
+
+internal record struct ValidateSecretReferencePayload()
+{
+ [JsonPropertyName("secret_reference")]
+ public required string SecretReference { get; set; }
+
+ internal static string ToJson(ValidateSecretReferencePayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ValidateSecretReferencePayload);
+ internal static ValidateSecretReferencePayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ValidateSecretReferencePayload);
+}
+
+internal record struct GetAllItemsPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("item_ids")]
+ public required IReadOnlyList ItemIds { get; set; }
+
+ internal static string ToJson(GetAllItemsPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.GetAllItemsPayload);
+ internal static GetAllItemsPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.GetAllItemsPayload);
+}
+
+internal record struct GetItemsPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; set; }
+
+ internal static string ToJson(GetItemsPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.GetItemsPayload);
+ internal static GetItemsPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.GetItemsPayload);
+}
+
+internal record struct GetItemPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("item_id")]
+ public required IReadOnlyList ItemIds { get; set; }
+
+ internal static string ToJson(GetItemPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.GetItemPayload);
+ internal static GetItemPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.GetItemPayload);
+}
+
+internal record struct ListItemsPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("filters")]
+ public required IReadOnlyList Filters { get; set; }
+
+ internal static string ToJson(ListItemsPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ListItemsPayload);
+ internal static ListItemsPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ListItemsPayload);
+}
+
+internal record struct ItemsPutPayload()
+{
+ [JsonPropertyName("item")]
+ public required Item Item { get; set; }
+
+ internal static string ToJson(ItemsPutPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsPutPayload);
+ internal static ItemsPutPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsPutPayload);
+}
+
+internal record struct DeleteItemPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("item_id")]
+ public required string ItemId { get; set; }
+
+ internal static string ToJson(DeleteItemPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.DeleteItemPayload);
+ internal static DeleteItemPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.DeleteItemPayload);
+}
+
+internal record struct DeleteAllItemsPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("item_ids")]
+ public required IReadOnlyList ItemIds { get; set; }
+
+ internal static string ToJson(DeleteAllItemsPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.DeleteAllItemsPayload);
+ internal static DeleteAllItemsPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.DeleteAllItemsPayload);
+}
+
+internal record struct ItemsCreatePayload()
+{
+ [JsonPropertyName("params")]
+ public required ItemCreateParams Params { get; set; }
+
+ internal static string ToJson(ItemsCreatePayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsCreatePayload);
+ internal static ItemsCreatePayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsCreatePayload);
+}
+
+internal record struct ItemsCreateAllPayload()
+{
+ [JsonPropertyName("vault_id")]
+ public required string VaultId { get; set; }
+
+ [JsonPropertyName("params")]
+ public required IReadOnlyList Params { get; set; }
+
+ internal static string ToJson(ItemsCreateAllPayload value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ItemsCreateAllPayload);
+ internal static ItemsCreateAllPayload FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ItemsCreateAllPayload);
+}
+
diff --git a/src/Keyden.1Password.Sdk/Types/PasswordRecipe.cs b/src/Keyden.1Password.Sdk/Types/PasswordRecipe.cs
new file mode 100644
index 0000000..6bf35d9
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/PasswordRecipe.cs
@@ -0,0 +1,10 @@
+using System.Text.Json;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct PasswordRecipe()
+{
+
+ internal static string ToJson(PasswordRecipe value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.PasswordRecipe);
+ internal static PasswordRecipe FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.PasswordRecipe);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/ResolveSecretsResponse.cs b/src/Keyden.1Password.Sdk/Types/ResolveSecretsResponse.cs
new file mode 100644
index 0000000..d6f1dee
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/ResolveSecretsResponse.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public record struct ResolveSecretsResponse()
+{
+ public struct ResponseObject()
+ {
+ [JsonPropertyName("content")]
+ public ContentObject? Content { get; set; }
+ public struct ContentObject()
+ {
+ [JsonPropertyName("secret")]
+ public string Secret { get; set; } = string.Empty;
+
+ [JsonPropertyName("itemId")]
+ public string ItemId { get; set; } = string.Empty;
+
+ [JsonPropertyName("vaultId")]
+ public string VaultId { get; set; } = string.Empty;
+ }
+
+ [JsonPropertyName("error")]
+ public ErrorObject? Error { get; set; }
+ public struct ErrorObject()
+ {
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = string.Empty;
+ }
+ }
+
+ [JsonPropertyName("individualResponses")]
+ public required IReadOnlyDictionary IndividualResponses { get; set; }
+
+ internal static string ToJson(ResolveSecretsResponse value) => JsonSerializer.Serialize(value, OnePassTypesJsonContext.Default.ResolveSecretsResponse);
+ internal static ResolveSecretsResponse FromJson(string value) => JsonSerializer.Deserialize(value, OnePassTypesJsonContext.Default.ResolveSecretsResponse);
+}
diff --git a/src/Keyden.1Password.Sdk/Types/Response.cs b/src/Keyden.1Password.Sdk/Types/Response.cs
new file mode 100644
index 0000000..5844b82
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/Response.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct Response
+ where TValue : struct
+ where TError : struct
+{
+ [JsonPropertyName("content")]
+ public TValue? Content { get; set; }
+ [JsonPropertyName("error")]
+ public TError? Error { get; set; }
+}
diff --git a/src/Keyden.1Password.Sdk/Types/Website.cs b/src/Keyden.1Password.Sdk/Types/Website.cs
new file mode 100644
index 0000000..7c42459
--- /dev/null
+++ b/src/Keyden.1Password.Sdk/Types/Website.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Keyden.OnePassword.SDK.Types;
+
+public struct Website()
+{
+ [JsonPropertyName("url")]
+ public required string Url { get; set; }
+
+ [JsonPropertyName("label")]
+ public required string Label { get; set; }
+
+ [JsonPropertyName("autofillBehavior")]
+ [JsonConverter(typeof(AutofillBehaviorJsonConverter))]
+ public required AutofillBehavior AutofillBehavior { get; set; }
+}
diff --git a/src/Keyden.App/App.axaml.cs b/src/Keyden.App/App.axaml.cs
index a3175d9..b4ef007 100644
--- a/src/Keyden.App/App.axaml.cs
+++ b/src/Keyden.App/App.axaml.cs
@@ -18,6 +18,7 @@
using System.Reflection.Metadata;
using System.Threading;
using System.Threading.Tasks;
+using System.Diagnostics;
namespace Keyden;
@@ -155,7 +156,7 @@ private async void MainAppPipeThread(CancellationToken ct)
public override void OnFrameworkInitializationCompleted()
{
Exception? singleInstanceEx = null;
- if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime)
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime && !Debugger.IsAttached)
{
try
{
diff --git a/src/Keyden.App/GenericSystemServices.cs b/src/Keyden.App/GenericSystemServices.cs
index 24bcde9..bdda447 100644
--- a/src/Keyden.App/GenericSystemServices.cs
+++ b/src/Keyden.App/GenericSystemServices.cs
@@ -78,4 +78,25 @@ public void NotifyPreauthorizedKey(ClientInfo clientInfo, SshKey key)
}
public string AuthenticationBranding => "None";
+
+ private readonly static string[] MacNativeMessagingPaths =
+ [
+ "~/Library/Application Support/Google/Chrome/NativeMessagingHosts",
+ "~/Library/Application Support/Chromium/NativeMessagingHosts",
+ "/Library/Google/Chrome/NativeMessagingHosts",
+ "/Library/Application Support/Chromium/NativeMessagingHosts",
+ ];
+ private readonly static string[] LinuxNativeMessagingPaths =
+ [
+ "~/.config/google-chrome/NativeMessagingHosts",
+ "~/.config/chromium/NativeMessagingHosts",
+ "/etc/opt/chrome/native-messaging-hosts",
+ "/etc/chromium/native-messaging-hosts",
+ ];
+ public string? GetNativeClientManifest(string id)
+ {
+ // throw new NotImplementedException();
+ return null;
+ }
+
}
diff --git a/src/Keyden.App/KeyStoreController.cs b/src/Keyden.App/KeyStoreController.cs
index e183ac9..24416cb 100644
--- a/src/Keyden.App/KeyStoreController.cs
+++ b/src/Keyden.App/KeyStoreController.cs
@@ -33,6 +33,7 @@ public sealed class KeyStoreController : ISshKeyStore, ISshKeyOptionsStore
private readonly DeveloperTestKeyStore DevTestStore;
private readonly OnePassCliSshKeyStore OnePassCliStore;
+ private readonly OnePassSdkSshKeyStore OnePassSdkStore;
private KeyStoreControllerData Data { get; }
private const string DataLocation = "keystore-controller-data.json";
@@ -46,6 +47,7 @@ public KeyStoreController(
DevTestStore = App.GetKeyedService("devtest");
OnePassCliStore = App.GetKeyedService("op");
+ OnePassSdkStore = App.GetKeyedService("opsdk");
if (FileSystem.TryReadBytes(DataLocation, out var contents))
{
@@ -63,7 +65,11 @@ public KeyStoreController(
else
Data = new();
+ // assign current settings
UpdateBackend();
+ OnePassSdkStore.AccountName = Settings.OnepassAccountName;
+
+ // listen for new settings
Settings.PropertyChanged += Settings_PropertyChanged;
}
@@ -71,10 +77,14 @@ private void UpdateBackend()
{
switch (Settings.KeystoreBackend)
{
- case KeystoreBackend.OnePassCLI:
+ case KeystoreBackend.OnePassCli:
BaseKeyStore = OnePassCliStore;
BaseOptionsStore = OnePassCliStore;
break;
+ case KeystoreBackend.OnePassSdk:
+ BaseKeyStore = OnePassSdkStore;
+ BaseOptionsStore = OnePassSdkStore;
+ break;
case KeystoreBackend.DeveloperTest:
BaseKeyStore = DevTestStore;
BaseOptionsStore = DevTestStore;
@@ -91,6 +101,8 @@ private void Settings_PropertyChanged(object? sender, PropertyChangedEventArgs e
{
if (e.PropertyName == nameof(Settings.KeystoreBackend))
UpdateBackend();
+ if (e.PropertyName == nameof(Settings.OnepassAccountName))
+ OnePassSdkStore.AccountName = Settings.OnepassAccountName;
}
public IReadOnlyList PublicKeys =>
diff --git a/src/Keyden.App/Keyden.App.csproj b/src/Keyden.App/Keyden.App.csproj
index bf28c61..f5425fe 100644
--- a/src/Keyden.App/Keyden.App.csproj
+++ b/src/Keyden.App/Keyden.App.csproj
@@ -1,9 +1,11 @@
-
+
- net9.0
+ net10.0
true
True
+
+ true
diff --git a/src/Keyden.App/KeydenSettings.cs b/src/Keyden.App/KeydenSettings.cs
index 77c225f..af3a362 100644
--- a/src/Keyden.App/KeydenSettings.cs
+++ b/src/Keyden.App/KeydenSettings.cs
@@ -3,7 +3,6 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Serialization;
-using System.Text.Unicode;
namespace Keyden;
@@ -16,6 +15,13 @@ public KeystoreBackend KeystoreBackend
set => SetProperty(ref _KeystoreBackend, value);
}
+ private string _OnepassAccountName = string.Empty;
+ public string OnepassAccountName
+ {
+ get => _OnepassAccountName;
+ set => SetProperty(ref _OnepassAccountName, value);
+ }
+
private float _AuthButtonEnableDelay = 0.5f;
public float AuthButtonEnableDelay
{
@@ -109,7 +115,8 @@ public enum KeystoreBackend
{
None,
DeveloperTest,
- OnePassCLI,
+ OnePassCli,
+ OnePassSdk,
}
public enum AuthenticationMode
diff --git a/src/Keyden.App/ServiceCollectionExtensions.cs b/src/Keyden.App/ServiceCollectionExtensions.cs
index b16a4c4..65a6b67 100644
--- a/src/Keyden.App/ServiceCollectionExtensions.cs
+++ b/src/Keyden.App/ServiceCollectionExtensions.cs
@@ -67,6 +67,7 @@ public static void AddKeydenServices(this IServiceCollection collection)
collection.AddKeyedSingleton("devtest");
collection.AddKeyedSingleton("op");
+ collection.AddKeyedSingleton("opsdk");
collection.AddSingleton();
collection.AddSingletonAlias();
diff --git a/src/Keyden.App/ViewModels/SettingsViewModel.cs b/src/Keyden.App/ViewModels/SettingsViewModel.cs
index 1b682b7..aa10f0f 100644
--- a/src/Keyden.App/ViewModels/SettingsViewModel.cs
+++ b/src/Keyden.App/ViewModels/SettingsViewModel.cs
@@ -25,6 +25,14 @@ public SettingsViewModel(
{
SystemServices = systemServices;
Settings = settings;
+
+ Settings.PropertyChanged += Settings_PropertyChanged;
+ }
+
+ private void Settings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(Settings.KeystoreBackend))
+ this.RaisePropertyChanged(nameof(UsingOnepassSdk));
}
private int _TabIndexSelected = 0;
@@ -53,11 +61,12 @@ public int TabIndexSelected
public bool IsSecurityTabSelectedAndLocked => TabIndexSelected == 2 && !SecurityUnlocked;
public bool IsSecurityTabSelectedAndUnlocked => TabIndexSelected == 2 && SecurityUnlocked;
public bool IsAdvancedTabSelected => TabIndexSelected == 1;
+ public bool UsingOnepassSdk => Settings.KeystoreBackend == KeystoreBackend.OnePassSdk;
public IReadOnlyList KeystoreBackends =>
Settings.DeveloperMode
- ? [KeystoreBackend.None, KeystoreBackend.OnePassCLI, KeystoreBackend.DeveloperTest]
- : [KeystoreBackend.None, KeystoreBackend.OnePassCLI];
+ ? [KeystoreBackend.None, KeystoreBackend.OnePassSdk, KeystoreBackend.OnePassCli, KeystoreBackend.DeveloperTest]
+ : [KeystoreBackend.None, KeystoreBackend.OnePassSdk, KeystoreBackend.OnePassCli];
public IReadOnlyList AuthenticationModes { get; } = [AuthenticationMode.System, AuthenticationMode.InternalPIN];
diff --git a/src/Keyden.App/Views/AuthPrompt.axaml.cs b/src/Keyden.App/Views/AuthPrompt.axaml.cs
index 3c9a121..e0f5077 100644
--- a/src/Keyden.App/Views/AuthPrompt.axaml.cs
+++ b/src/Keyden.App/Views/AuthPrompt.axaml.cs
@@ -178,6 +178,13 @@ private void Pin_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventA
Pin.Focus();
}
+ protected override void OnOpened(EventArgs e)
+ {
+ base.OnOpened(e);
+
+ Pin.Focus();
+ }
+
private void AuthPrompt_Closed(object? sender, EventArgs e)
{
AuthButtonEnabled = false;
diff --git a/src/Keyden.App/Views/SettingsWindow.axaml b/src/Keyden.App/Views/SettingsWindow.axaml
index 6ff2ffb..6df332e 100644
--- a/src/Keyden.App/Views/SettingsWindow.axaml
+++ b/src/Keyden.App/Views/SettingsWindow.axaml
@@ -86,10 +86,11 @@
-
-
- Key storage backend:
-
+ Key storage backend:
+
@@ -97,7 +98,12 @@
-
+
+
+
+ 1Password account
+
+
Automatically start
diff --git a/src/Keyden.App/Views/SettingsWindow.axaml.cs b/src/Keyden.App/Views/SettingsWindow.axaml.cs
index 6f11e33..23017d6 100644
--- a/src/Keyden.App/Views/SettingsWindow.axaml.cs
+++ b/src/Keyden.App/Views/SettingsWindow.axaml.cs
@@ -94,7 +94,8 @@ object IValueConverter.Convert(object? value, Type targetType, object? parameter
{
KeystoreBackend.None => "None",
KeystoreBackend.DeveloperTest => "Developer Test",
- KeystoreBackend.OnePassCLI => "1Password CLI",
+ KeystoreBackend.OnePassCli => "1Password CLI",
+ KeystoreBackend.OnePassSdk => "1Password SDK",
_ => backend.ToString(),
},
AuthenticationMode authMode => authMode switch
diff --git a/src/Keyden.App/Views/TitleBarButtons.axaml.cs b/src/Keyden.App/Views/TitleBarButtons.axaml.cs
index c0cf21a..e1e92a3 100644
--- a/src/Keyden.App/Views/TitleBarButtons.axaml.cs
+++ b/src/Keyden.App/Views/TitleBarButtons.axaml.cs
@@ -41,7 +41,10 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
if (e.Root is not Window window)
return;
- bool usingClientChrome = window.IsExtendedIntoWindowDecorations;
+ bool usingClientChrome =
+ window.IsExtendedIntoWindowDecorations ||
+ (window.PlatformImpl?.IsClientAreaExtendedToDecorations ?? false);
+
MinimizeButton.IsVisible = usingClientChrome;
MaximizeButton.IsVisible = usingClientChrome;
CloseButton.IsVisible = usingClientChrome;
diff --git a/src/Keyden.Core/AgentProtocol/AgentClient.cs b/src/Keyden.Core/AgentProtocol/AgentClient.cs
index d6b9852..ea406ca 100644
--- a/src/Keyden.Core/AgentProtocol/AgentClient.cs
+++ b/src/Keyden.Core/AgentProtocol/AgentClient.cs
@@ -7,6 +7,7 @@
namespace Keyden.AgentProtocol;
// https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#name-private-key-operations
+// https://datatracker.ietf.org/doc/html/draft-ietf-sshm-ssh-agent
// https://www.rfc-editor.org/rfc/rfc4251.html
internal enum AgentMessageType : byte
{
diff --git a/src/Keyden.Core/AgentProtocol/AgentProtocolPrimitiveExtensions.cs b/src/Keyden.Core/AgentProtocol/AgentProtocolPrimitiveExtensions.cs
index 098983a..e97cabf 100644
--- a/src/Keyden.Core/AgentProtocol/AgentProtocolPrimitiveExtensions.cs
+++ b/src/Keyden.Core/AgentProtocol/AgentProtocolPrimitiveExtensions.cs
@@ -16,6 +16,16 @@ public static void WriteByte(this BufferWriter self, byte value)
self.NextBlock(1).Span[0] = value;
}
+ public static Memory ReadBytes(this BufferWriter self, int length)
+ {
+ return self.NextBlock(length);
+ }
+ public static void WriteBytes(this BufferWriter self, ReadOnlySpan value)
+ {
+ var block = self.NextBlock(value.Length);
+ value.CopyTo(block.Span);
+ }
+
public static bool ReadBool(this BufferReader self)
{
return self.ReadByte() != 0;
diff --git a/src/Keyden.Core/ISystemServices.cs b/src/Keyden.Core/ISystemServices.cs
index 5f03b2b..919c23b 100644
--- a/src/Keyden.Core/ISystemServices.cs
+++ b/src/Keyden.Core/ISystemServices.cs
@@ -25,6 +25,8 @@ Task TryAuthUser(
Process? GetPipeClientProcess(NamedPipeServerStream pipeServer);
Process? GetParentProcess(Process process);
+
+ string? GetNativeClientManifest(string id);
}
public struct AuthResult
diff --git a/src/Keyden.Core/KeyOptionsJson.cs b/src/Keyden.Core/KeyOptionsJson.cs
new file mode 100644
index 0000000..27aeb4b
--- /dev/null
+++ b/src/Keyden.Core/KeyOptionsJson.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text.Json.Nodes;
+
+namespace Keyden;
+
+file static class JsonExtensionMethods
+{
+ public static JsonValue AsJsonValue(this TimeSpan value)
+ {
+ return JsonValue.Create(value.ToString(null, CultureInfo.InvariantCulture));
+ }
+ public static JsonValue AsJsonValue(this bool value)
+ {
+ return JsonValue.Create(value);
+ }
+ public static JsonArray AsJsonValue(this IEnumerable value)
+ {
+ return [.. value.Select(x => JsonValue.Create(x))];
+ }
+ public static bool? GetBool(this JsonNode node)
+ {
+ if (node.AsValue() is not JsonValue value)
+ return null;
+ return value.GetValue();
+ }
+ public static TimeSpan? GetTimeSpan(this JsonNode node)
+ {
+ if (node.AsValue() is not JsonValue value)
+ return null;
+ var str = value.GetValue();
+ return TimeSpan.Parse(str, CultureInfo.InvariantCulture);
+ }
+ public static IReadOnlyList? GetStringList(this JsonNode node)
+ {
+ if (node.AsArray() is not JsonArray array)
+ return null;
+ return array
+ .Select(x => x?.GetValue())
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x!)
+ .ToList();
+ }
+}
+
+public static class KeyOptionsJson
+{
+ public static SshKeyOptions ReadOptionsNode(JsonObject obj)
+ {
+ var @default = new SshKeyOptions();
+ return new SshKeyOptions()
+ {
+ EnableForMachines = obj["EnableForMachines"]?.GetStringList() ?? @default.EnableForMachines,
+ RequireAuthorization = obj["RequireAuthorization"]?.GetBool() ?? @default.RequireAuthentication,
+ RemainAuthorized = obj["RemainAuthorized"]?.GetBool() ?? @default.RemainAuthorized,
+ RemainAuthorizedFor = obj["RemainAuthorizedFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedFor,
+ RemainAuthorizedUntilKeyInactivity = obj["RemainAuthorizedUntilKeyInactivity"]?.GetBool() ?? @default.RemainAuthorizedUntilKeyInactivity,
+ RemainAuthorizedUntilKeyInactivityFor = obj["RemainAuthorizedUntilKeyInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedUntilKeyInactivityFor,
+ RemainAuthorizedUntilUserInactivity = obj["RemainAuthorizedUntilUserInactivity"]?.GetBool() ?? @default.RemainAuthorizedUntilUserInactivity,
+ RemainAuthorizedUntilUserInactivityFor = obj["RemainAuthorizedUntilUserInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedUntilUserInactivityFor,
+ RemainAuthorizedUntilLocked = obj["RemainAuthorizedUntilLocked"]?.GetBool() ?? @default.RemainAuthorizedUntilLocked,
+ RequireAuthentication = obj["RequireAuthentication"]?.GetBool() ?? @default.RequireAuthentication,
+ RemainAuthenticated = obj["RemainAuthenticated"]?.GetBool() ?? @default.RemainAuthenticated,
+ RemainAuthenticatedFor = obj["RemainAuthenticatedFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedFor,
+ RemainAuthenticatedUntilKeyInactivity = obj["RemainAuthenticatedUntilKeyInactivity"]?.GetBool() ?? @default.RemainAuthenticatedUntilKeyInactivity,
+ RemainAuthenticatedUntilKeyInactivityFor = obj["RemainAuthenticatedUntilKeyInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedUntilKeyInactivityFor,
+ RemainAuthenticatedUntilUserInactivity = obj["RemainAuthenticatedUntilUserInactivity"]?.GetBool() ?? @default.RemainAuthenticatedUntilUserInactivity,
+ RemainAuthenticatedUntilUserInactivityFor = obj["RemainAuthenticatedUntilUserInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedUntilUserInactivityFor,
+ RemainAuthenticatedUntilLocked = obj["RemainAuthenticatedUntilLocked"]?.GetBool() ?? @default.RemainAuthenticatedUntilLocked,
+ };
+ }
+
+ public static void WriteOptionsNode(JsonObject obj, SshKeyOptions opts)
+ {
+ obj["EnableForMachines"] = opts.EnableForMachines.AsJsonValue();
+ obj["RequireAuthorization"] = opts.RequireAuthorization.AsJsonValue();
+ obj["RemainAuthorized"] = opts.RemainAuthorized.AsJsonValue();
+ obj["RemainAuthorizedFor"] = opts.RemainAuthorizedFor.AsJsonValue();
+ obj["RemainAuthorizedUntilKeyInactivity"] = opts.RemainAuthorizedUntilKeyInactivity.AsJsonValue();
+ obj["RemainAuthorizedUntilKeyInactivityFor"] = opts.RemainAuthorizedUntilKeyInactivityFor.AsJsonValue();
+ obj["RemainAuthorizedUntilUserInactivity"] = opts.RemainAuthorizedUntilUserInactivity.AsJsonValue();
+ obj["RemainAuthorizedUntilUserInactivityFor"] = opts.RemainAuthorizedUntilUserInactivityFor.AsJsonValue();
+ obj["RemainAuthorizedUntilLocked"] = opts.RemainAuthorizedUntilLocked.AsJsonValue();
+ obj["RequireAuthentication"] = opts.RequireAuthentication.AsJsonValue();
+ obj["RemainAuthenticated"] = opts.RemainAuthenticated.AsJsonValue();
+ obj["RemainAuthenticatedFor"] = opts.RemainAuthenticatedFor.AsJsonValue();
+ obj["RemainAuthenticatedUntilKeyInactivity"] = opts.RemainAuthenticatedUntilKeyInactivity.AsJsonValue();
+ obj["RemainAuthenticatedUntilKeyInactivityFor"] = opts.RemainAuthenticatedUntilKeyInactivityFor.AsJsonValue();
+ obj["RemainAuthenticatedUntilUserInactivity"] = opts.RemainAuthenticatedUntilUserInactivity.AsJsonValue();
+ obj["RemainAuthenticatedUntilUserInactivityFor"] = opts.RemainAuthenticatedUntilUserInactivityFor.AsJsonValue();
+ obj["RemainAuthenticatedUntilLocked"] = opts.RemainAuthenticatedUntilLocked.AsJsonValue();
+ }
+}
diff --git a/src/Keyden.Core/Keyden.Core.csproj b/src/Keyden.Core/Keyden.Core.csproj
index 5fa3091..d2f18c3 100644
--- a/src/Keyden.Core/Keyden.Core.csproj
+++ b/src/Keyden.Core/Keyden.Core.csproj
@@ -1,7 +1,7 @@
-
+
- net9.0
+ net10.0
Keyden
True
@@ -11,4 +11,8 @@
+
+
+
+
diff --git a/src/Keyden.Core/OnePassCliSshKeyStore.cs b/src/Keyden.Core/OnePassCliSshKeyStore.cs
index c0f0e66..363779a 100644
--- a/src/Keyden.Core/OnePassCliSshKeyStore.cs
+++ b/src/Keyden.Core/OnePassCliSshKeyStore.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
-using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
@@ -12,48 +10,6 @@
namespace Keyden;
-file static class JsonExtensionMethods
-{
- public static JsonValue AsJsonValue(this TimeSpan value)
- {
- return JsonValue.Create(value.ToString(null, CultureInfo.InvariantCulture));
- }
- public static JsonValue AsJsonValue(this bool value)
- {
- return JsonValue.Create(value);
- }
- public static JsonArray AsJsonValue(this IReadOnlyList value)
- {
- var array = new JsonNode[value.Count];
- for (int i = 0; i < value.Count; i++)
- array[i] = JsonValue.Create(value[i]);
- return new JsonArray(array);
- }
- public static bool? GetBool(this JsonNode node)
- {
- if (node.AsValue() is not JsonValue value)
- return null;
- return value.GetValue();
- }
- public static TimeSpan? GetTimeSpan(this JsonNode node)
- {
- if (node.AsValue() is not JsonValue value)
- return null;
- var str = value.GetValue();
- return TimeSpan.Parse(str, CultureInfo.InvariantCulture);
- }
- public static IReadOnlyList? GetStringList(this JsonNode node)
- {
- if (node.AsArray() is not JsonArray array)
- return null;
- return array
- .Select(x => x?.GetValue())
- .Where(x => !string.IsNullOrWhiteSpace(x))
- .Select(x => x!)
- .ToList();
- }
-}
-
public sealed class OnePassCliSshKeyStore : ISshKeyStore, ISshKeyOptionsStore
{
private readonly static UTF8Encoding Utf8 = new(encoderShouldEmitUTF8Identifier: false);
@@ -244,51 +200,6 @@ void ISshKeyOptionsStore.SetKeyOptions(string id, SshKeyOptions? options)
}
private const string KewardenOptionsEntryName = "\"Keyden Options\"";
- private static SshKeyOptions ReadOptionsNode(JsonObject obj)
- {
- var @default = new SshKeyOptions();
- return new SshKeyOptions()
- {
- EnableForMachines = obj["EnableForMachines"]?.GetStringList() ?? @default.EnableForMachines,
- RequireAuthorization = obj["RequireAuthorization"]?.GetBool() ?? @default.RequireAuthentication,
- RemainAuthorized = obj["RemainAuthorized"]?.GetBool() ?? @default.RemainAuthorized,
- RemainAuthorizedFor = obj["RemainAuthorizedFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedFor,
- RemainAuthorizedUntilKeyInactivity = obj["RemainAuthorizedUntilKeyInactivity"]?.GetBool() ?? @default.RemainAuthorizedUntilKeyInactivity,
- RemainAuthorizedUntilKeyInactivityFor = obj["RemainAuthorizedUntilKeyInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedUntilKeyInactivityFor,
- RemainAuthorizedUntilUserInactivity = obj["RemainAuthorizedUntilUserInactivity"]?.GetBool() ?? @default.RemainAuthorizedUntilUserInactivity,
- RemainAuthorizedUntilUserInactivityFor = obj["RemainAuthorizedUntilUserInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthorizedUntilUserInactivityFor,
- RemainAuthorizedUntilLocked = obj["RemainAuthorizedUntilLocked"]?.GetBool() ?? @default.RemainAuthorizedUntilLocked,
- RequireAuthentication = obj["RequireAuthentication"]?.GetBool() ?? @default.RequireAuthentication,
- RemainAuthenticated = obj["RemainAuthenticated"]?.GetBool() ?? @default.RemainAuthenticated,
- RemainAuthenticatedFor = obj["RemainAuthenticatedFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedFor,
- RemainAuthenticatedUntilKeyInactivity = obj["RemainAuthenticatedUntilKeyInactivity"]?.GetBool() ?? @default.RemainAuthenticatedUntilKeyInactivity,
- RemainAuthenticatedUntilKeyInactivityFor = obj["RemainAuthenticatedUntilKeyInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedUntilKeyInactivityFor,
- RemainAuthenticatedUntilUserInactivity = obj["RemainAuthenticatedUntilUserInactivity"]?.GetBool() ?? @default.RemainAuthenticatedUntilUserInactivity,
- RemainAuthenticatedUntilUserInactivityFor = obj["RemainAuthenticatedUntilUserInactivityFor"]?.GetTimeSpan() ?? @default.RemainAuthenticatedUntilUserInactivityFor,
- RemainAuthenticatedUntilLocked = obj["RemainAuthenticatedUntilLocked"]?.GetBool() ?? @default.RemainAuthenticatedUntilLocked,
- };
- }
-
- private static void WriteOptionsNode(JsonObject obj, SshKeyOptions opts)
- {
- obj["EnableForMachines"] = opts.EnableForMachines.AsJsonValue();
- obj["RequireAuthorization"] = opts.RequireAuthorization.AsJsonValue();
- obj["RemainAuthorized"] = opts.RemainAuthorized.AsJsonValue();
- obj["RemainAuthorizedFor"] = opts.RemainAuthorizedFor.AsJsonValue();
- obj["RemainAuthorizedUntilKeyInactivity"] = opts.RemainAuthorizedUntilKeyInactivity.AsJsonValue();
- obj["RemainAuthorizedUntilKeyInactivityFor"] = opts.RemainAuthorizedUntilKeyInactivityFor.AsJsonValue();
- obj["RemainAuthorizedUntilUserInactivity"] = opts.RemainAuthorizedUntilUserInactivity.AsJsonValue();
- obj["RemainAuthorizedUntilUserInactivityFor"] = opts.RemainAuthorizedUntilUserInactivityFor.AsJsonValue();
- obj["RemainAuthorizedUntilLocked"] = opts.RemainAuthorizedUntilLocked.AsJsonValue();
- obj["RequireAuthentication"] = opts.RequireAuthentication.AsJsonValue();
- obj["RemainAuthenticated"] = opts.RemainAuthenticated.AsJsonValue();
- obj["RemainAuthenticatedFor"] = opts.RemainAuthenticatedFor.AsJsonValue();
- obj["RemainAuthenticatedUntilKeyInactivity"] = opts.RemainAuthenticatedUntilKeyInactivity.AsJsonValue();
- obj["RemainAuthenticatedUntilKeyInactivityFor"] = opts.RemainAuthenticatedUntilKeyInactivityFor.AsJsonValue();
- obj["RemainAuthenticatedUntilUserInactivity"] = opts.RemainAuthenticatedUntilUserInactivity.AsJsonValue();
- obj["RemainAuthenticatedUntilUserInactivityFor"] = opts.RemainAuthenticatedUntilUserInactivityFor.AsJsonValue();
- obj["RemainAuthenticatedUntilLocked"] = opts.RemainAuthenticatedUntilLocked.AsJsonValue();
- }
async Task ISshKeyOptionsStore.SyncKeyOptions(CancellationToken ct)
{
@@ -375,14 +286,14 @@ async Task ISshKeyOptionsStore.SyncKeyOptions(CancellationToken ct)
continue;
var keyId = field.Key;
- var upstreamOptions = ReadOptionsNode(obj);
+ var upstreamOptions = KeyOptionsJson.ReadOptionsNode(obj);
unprocessedKeys.Remove(keyId);
if (DirtyOptions.Contains(keyId))
{
if (Options.TryGetValue(keyId, out var downstreamOptions))
- WriteOptionsNode(obj, downstreamOptions);
+ KeyOptionsJson.WriteOptionsNode(obj, downstreamOptions);
else
toRemoveKeys.Add(keyId);
}
@@ -397,7 +308,7 @@ async Task ISshKeyOptionsStore.SyncKeyOptions(CancellationToken ct)
changed = true;
var obj = new JsonObject();
var downstreamOptions = Options[unprocessedKey];
- WriteOptionsNode(obj, downstreamOptions);
+ KeyOptionsJson.WriteOptionsNode(obj, downstreamOptions);
jsonDoc[unprocessedKey] = obj;
}
diff --git a/src/Keyden.Core/OnePassSdkSshKeyStore.cs b/src/Keyden.Core/OnePassSdkSshKeyStore.cs
new file mode 100644
index 0000000..66e4a2e
--- /dev/null
+++ b/src/Keyden.Core/OnePassSdkSshKeyStore.cs
@@ -0,0 +1,305 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Keyden.OnePassword.SDK;
+using Keyden.OnePassword.SDK.Types;
+
+namespace Keyden;
+
+// https://developer.1password.com/docs/sdks/desktop-app-integrations
+public sealed partial class OnePassSdkSshKeyStore : ISshKeyStore, ISshKeyOptionsStore
+{
+ [JsonSerializable(typeof(PrivateKeyInfo))]
+ internal partial class JsonContext : JsonSerializerContext
+ {
+ }
+
+ internal record struct PrivateKeyInfo()
+ {
+ [JsonPropertyName("publicKey")]
+ public required string PublicKey { get; set; }
+
+ [JsonPropertyName("fingerprint")]
+ public required string Fingerprint { get; set; }
+
+ [JsonPropertyName("keyType")]
+ public required string KeyType { get; set; }
+ }
+
+ private ISystemServices SystemServices { get; }
+ private OnePassSdk Sdk { get; }
+ public string AccountName
+ {
+ get => Sdk.AccountName ?? string.Empty;
+ set => Sdk.AccountName = value;
+ }
+
+ public OnePassSdkSshKeyStore(ISystemServices systemServices)
+ {
+ SystemServices = systemServices;
+ Sdk = new OnePassSdk(new()
+ {
+ IntegrationName = "Keyden",
+ IntegrationVersion = Verlite.Version.Full,
+ });
+ }
+
+ private readonly static UTF8Encoding Utf8 = new(encoderShouldEmitUTF8Identifier: false);
+
+ private List PublicKeys { get; } = [];
+ private List PrivateKeys { get; } = [];
+
+ IReadOnlyList ISshKeyStore.PublicKeys => PublicKeys;
+
+ ValueTask ISshKeyStore.GetPrivateKey(SshKey publicKey, CancellationToken ct)
+ {
+ var matchingKey = PrivateKeys
+ .Where(k => k.PublicKey.Span.SequenceEqual(publicKey.PublicKey.Span))
+ .FirstOrDefault();
+
+ return new(matchingKey);
+ }
+
+ async Task ISshKeyStore.SyncKeys(CancellationToken ct)
+ {
+ var sw = Stopwatch.StartNew();
+
+ var newIds = new List();
+ var newKeys = new List();
+
+ foreach (var vault in await Sdk.GetAllVaultsAsync())
+ {
+ var itemIds = new List();
+ foreach (var itemOverview in await Sdk.ListItemsAsync(vault.Id, []))
+ if (itemOverview.Category == ItemCategory.SshKey)
+ itemIds.Add(itemOverview.Id);
+
+ if (itemIds.Count == 0)
+ continue;
+
+ var items = await Sdk.GetItemsAsync(vault.Id, itemIds);
+
+ foreach (var itemResponse in items.IndividualResponses)
+ {
+ if (itemResponse.Content is null)
+ continue;
+
+ var item = itemResponse.Content.Value;
+ string? fingerprint = null;
+ string? publicKey = null;
+ string? privateKey = null;
+
+ foreach (var field in item.Fields)
+ {
+ if (field.FieldType != ItemFieldType.SshKey)
+ continue;
+
+ if (field.Id is "public_key")
+ publicKey = field.Value;
+ else if (field.Id is "private_key")
+ {
+ var privateKeySecretRef = $"op://{vault.Id}/{item.Id}/{field.Id}?ssh-format=openssh";
+ privateKey = await Sdk.ResolveSecretAsync(privateKeySecretRef).WaitAsync(ct);
+
+ if (field.Details.HasValue && field.Details.Value.Type is "SshKey")
+ {
+ try
+ {
+ var keyInfo = JsonSerializer.Deserialize(field.Details.Value.Content, JsonContext.Default.PrivateKeyInfo);
+ publicKey = keyInfo.PublicKey;
+ fingerprint = keyInfo.Fingerprint;
+ }
+ catch (JsonException)
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ if (publicKey is null || privateKey is null || fingerprint is null)
+ continue;
+
+ newIds.Add(item.Id);
+ newKeys.Add(new()
+ {
+ Id = item.Id,
+ Name = item.Title,
+ Fingerprint = fingerprint,
+ PublicKeyText = publicKey,
+ PublicKey = Convert.FromBase64String(publicKey[publicKey.IndexOf(' ')..]),
+ PrivateKey = Utf8.GetBytes(privateKey),
+ });
+ }
+ }
+
+ PrivateKeys.Clear();
+ PublicKeys.Clear();
+ PrivateKeys.AddRange(newKeys);
+ PublicKeys.AddRange(newKeys.Select(k => k with { PrivateKey = default }));
+
+ Debug.WriteLine($"Synced with op in {sw.Elapsed.TotalSeconds} seconds");
+ }
+
+ private readonly HashSet DirtyOptions = [];
+ private readonly Dictionary Options = [];
+ private const string KeydenOptionsEntryName = "Keyden Options";
+
+ SshKeyOptions? ISshKeyOptionsStore.GetKeyOptions(string id)
+ {
+ if (Options.TryGetValue(id, out var options))
+ return options;
+ else
+ return null;
+ }
+
+ void ISshKeyOptionsStore.SetKeyOptions(string id, SshKeyOptions? options)
+ {
+ if (options is null)
+ {
+ if (Options.Remove(id))
+ DirtyOptions.Add(id);
+ }
+ else
+ {
+
+ if (Options.TryGetValue(id, out var oldOptions) && options.Value != oldOptions)
+ DirtyOptions.Add(id);
+ else if (options.Value != new SshKeyOptions())
+ DirtyOptions.Add(id);
+
+ Options[id] = options.Value;
+ }
+ }
+
+ async Task ISshKeyOptionsStore.SyncKeyOptions(CancellationToken ct)
+ {
+ var jsonSerializerOptions = new JsonSerializerOptions()
+ {
+ WriteIndented = true,
+ };
+
+
+ var personalVaultId = string.Empty;
+ foreach (var vault in await Sdk.GetAllVaultsAsync().WaitAsync(ct))
+ {
+ if (vault.VaultType == "personal")
+ {
+ personalVaultId = vault.Id;
+ break;
+ }
+ }
+ if (personalVaultId is null)
+ throw new BackendException("SyncKeyOptions(): Could not find personal vault");
+
+ Item? optionsItem = null;
+ foreach (var item in await Sdk.ListItemsAsync(personalVaultId, []).WaitAsync(ct))
+ {
+ if (item.Category == ItemCategory.SecureNote && item.Title is KeydenOptionsEntryName)
+ {
+ optionsItem = await Sdk.GetItemAsync(personalVaultId, item.Id);
+ break;
+ }
+ }
+
+ if (!optionsItem.HasValue)
+ {
+ optionsItem = Sdk.CreateItem(new()
+ {
+ Title = KeydenOptionsEntryName,
+ VaultId = personalVaultId,
+ Category = ItemCategory.SecureNote,
+ Fields = [
+ new ItemField
+ {
+ Id = "json",
+ Title = "json",
+ FieldType = ItemFieldType.Text,
+ Value = "{}",
+ SectionId = "",
+ }
+ ],
+ Sections = [
+ new() { Id = "", Title = "" }
+ ]
+ });
+ }
+
+ var optionsJson = string.Empty;
+ var jsonFieldIndex = -1;
+
+ for (int i = 0; i < optionsItem.Value.Fields.Count; i++)
+ {
+ var field = optionsItem.Value.Fields[i];
+
+ optionsJson = field.Value;
+ jsonFieldIndex = i;
+ break;
+ }
+
+ if (jsonFieldIndex < 0)
+ throw new BackendException("Options \"Keyden Options\" -> \"json\" does not exist, automatic creation not yet supported. Please create manually");
+
+ bool changed = DirtyOptions.Count > 0;
+ JsonObject jsonDoc;
+
+ if (string.IsNullOrEmpty(optionsJson))
+ jsonDoc = new JsonObject();
+ else
+ jsonDoc = JsonNode.Parse(optionsJson)?.AsObject()
+ ?? throw new BackendException($"Failed to parse options json");
+
+ var unprocessedKeys = new HashSet(Options.Keys);
+ var toRemoveKeys = new HashSet();
+
+ foreach (var field in jsonDoc)
+ {
+ if (field.Value?.AsObject() is not JsonObject obj)
+ continue;
+
+ var keyId = field.Key;
+ var upstreamOptions = KeyOptionsJson.ReadOptionsNode(obj);
+
+ unprocessedKeys.Remove(keyId);
+
+ if (DirtyOptions.Contains(keyId))
+ {
+ if (Options.TryGetValue(keyId, out var downstreamOptions))
+ KeyOptionsJson.WriteOptionsNode(obj, downstreamOptions);
+ else
+ toRemoveKeys.Add(keyId);
+ }
+ else
+ Options[keyId] = upstreamOptions;
+ }
+
+ foreach (var removedKey in toRemoveKeys)
+ jsonDoc.Remove(removedKey);
+ foreach (var unprocessedKey in unprocessedKeys)
+ {
+ changed = true;
+ var obj = new JsonObject();
+ var downstreamOptions = Options[unprocessedKey];
+ KeyOptionsJson.WriteOptionsNode(obj, downstreamOptions);
+ jsonDoc[unprocessedKey] = obj;
+ }
+
+ if (changed)
+ {
+ var fieldsClone = optionsItem.Value.Fields.ToArray();
+ fieldsClone[jsonFieldIndex].Value = jsonDoc.ToJsonString(jsonSerializerOptions);
+ optionsItem = optionsItem.Value with { Fields = fieldsClone };
+
+ DirtyOptions.Clear();
+ await Sdk.UpdateItemAsync(optionsItem.Value);
+ }
+ }
+}
diff --git a/src/Keyden.Core/SshAgent.cs b/src/Keyden.Core/SshAgent.cs
index 296f47b..b5051a3 100644
--- a/src/Keyden.Core/SshAgent.cs
+++ b/src/Keyden.Core/SshAgent.cs
@@ -7,11 +7,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Channels;
diff --git a/src/Keyden.Desktop.Unix/Keyden.Desktop.Unix.csproj b/src/Keyden.Desktop.Unix/Keyden.Desktop.Unix.csproj
index e49cf7c..8f22382 100644
--- a/src/Keyden.Desktop.Unix/Keyden.Desktop.Unix.csproj
+++ b/src/Keyden.Desktop.Unix/Keyden.Desktop.Unix.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
WinExe
GPLv3
diff --git a/src/Keyden.Desktop.Unix/Program.cs b/src/Keyden.Desktop.Unix/Program.cs
index 492ef91..0d8f1fd 100644
--- a/src/Keyden.Desktop.Unix/Program.cs
+++ b/src/Keyden.Desktop.Unix/Program.cs
@@ -1,3 +1,5 @@
+using System;
+
using Avalonia;
using Avalonia.ReactiveUI;
@@ -5,9 +7,6 @@
using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign;
-using System;
-using System.Runtime.InteropServices;
-
namespace Keyden.Desktop.Any;
internal sealed class Program
@@ -16,8 +15,10 @@ internal sealed class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
- public static void Main(string[] args) => BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
+ public static int Main(string[] args)
+ {
+ return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ }
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
diff --git a/src/Keyden.Desktop.Windows/Keyden.Desktop.Windows.csproj b/src/Keyden.Desktop.Windows/Keyden.Desktop.Windows.csproj
index 0ea0b9d..eede2bc 100644
--- a/src/Keyden.Desktop.Windows/Keyden.Desktop.Windows.csproj
+++ b/src/Keyden.Desktop.Windows/Keyden.Desktop.Windows.csproj
@@ -1,7 +1,7 @@
-
+
- net9.0-windows10.0.20348.0
+ net10.0-windows10.0.20348.0
WinExe
GPLv3
en
diff --git a/src/Keyden.Desktop.Windows/Program.cs b/src/Keyden.Desktop.Windows/Program.cs
index 05f9349..c95b14d 100644
--- a/src/Keyden.Desktop.Windows/Program.cs
+++ b/src/Keyden.Desktop.Windows/Program.cs
@@ -1,12 +1,14 @@
+using System;
+
using Avalonia;
using Avalonia.ReactiveUI;
+using Keyden.OnePassword.SDK;
+
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign;
-using System;
-
namespace Keyden.Desktop.Windows;
internal sealed class Program
@@ -15,8 +17,10 @@ internal sealed class Program
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
- public static void Main(string[] args) => BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(args);
+ public static int Main(string[] args)
+ {
+ return BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ }
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
diff --git a/src/Keyden.Desktop.Windows/WindowsSystemServices.cs b/src/Keyden.Desktop.Windows/WindowsSystemServices.cs
index 4b161e8..508c676 100644
--- a/src/Keyden.Desktop.Windows/WindowsSystemServices.cs
+++ b/src/Keyden.Desktop.Windows/WindowsSystemServices.cs
@@ -290,5 +290,43 @@ public void NotifyPreauthorizedKey(ClientInfo clientInfo, SshKey key)
// TODO: popup a notification if the setting is enabled
}
+ private readonly static string[] WindowsNativeMessagingPaths =
+ [
+ @"SOFTWARE\Mozilla\NativeMessagingHosts",
+ @"SOFTWARE\Google\Chrome\NativeMessagingHosts",
+ ];
+
+ public string? GetNativeClientManifest(string id)
+ {
+ foreach (var path in WindowsNativeMessagingPaths)
+ {
+ RegistryKey? key = null;
+
+ try
+ {
+ key = Registry.CurrentUser.OpenSubKey(@$"{path}\{id}", false)!;
+ if (key is not null && key.GetValue(null) is string keyValue)
+ return keyValue;
+ }
+ finally
+ {
+ key?.Dispose();
+ }
+
+ try
+ {
+ key = Registry.LocalMachine.OpenSubKey(@$"{path}\{id}", false)!;
+ if (key is not null && key.GetValue(null) is string keyValue)
+ return keyValue;
+ }
+ finally
+ {
+ key?.Dispose();
+ }
+ }
+
+ return null;
+ }
+
public string AuthenticationBranding => "Windows Hello";
}
diff --git a/tests/SshAgentTest/SshAgentTest.csproj b/tests/SshAgentTest/SshAgentTest.csproj
index 69d7bde..9b0fdc1 100644
--- a/tests/SshAgentTest/SshAgentTest.csproj
+++ b/tests/SshAgentTest/SshAgentTest.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
Exe
diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj
index 66a2a2f..274656e 100644
--- a/tests/UnitTests/UnitTests.csproj
+++ b/tests/UnitTests/UnitTests.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
false
$(NoWarn);1701;1702;IDE0051;IDE0058;
@@ -27,12 +27,12 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all