From c2613aab129c8d1d5aba3b7ed02609059a826c84 Mon Sep 17 00:00:00 2001 From: Rory& Date: Wed, 16 Jul 2025 19:00:05 +0200 Subject: Federation tuff --- LibMatrix/Abstractions/VersionedKeyId.cs | 24 +++++++ LibMatrix/Abstractions/VersionedPublicKey.cs | 16 +++++ LibMatrix/Homeservers/FederationClient.cs | 79 +--------------------- LibMatrix/LibMatrix.csproj | 4 +- .../Responses/Federation/ServerKeysResponse.cs | 55 +++++++++++++++ .../Responses/Federation/ServerVersionResponse.cs | 16 +++++ LibMatrix/Responses/Federation/SignedObject.cs | 68 +++++++++++++++++++ LibMatrix/Services/HomeserverProviderService.cs | 1 + 8 files changed, 184 insertions(+), 79 deletions(-) create mode 100644 LibMatrix/Abstractions/VersionedKeyId.cs create mode 100644 LibMatrix/Abstractions/VersionedPublicKey.cs create mode 100644 LibMatrix/Responses/Federation/ServerKeysResponse.cs create mode 100644 LibMatrix/Responses/Federation/ServerVersionResponse.cs create mode 100644 LibMatrix/Responses/Federation/SignedObject.cs (limited to 'LibMatrix') diff --git a/LibMatrix/Abstractions/VersionedKeyId.cs b/LibMatrix/Abstractions/VersionedKeyId.cs new file mode 100644 index 0000000..0a6d651 --- /dev/null +++ b/LibMatrix/Abstractions/VersionedKeyId.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace LibMatrix.Abstractions; + +[DebuggerDisplay("{Algorithm}:{KeyId}")] +[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] +public class VersionedKeyId { + public required string Algorithm { get; set; } + public required string KeyId { get; set; } + + public static implicit operator VersionedKeyId(string key) { + var parts = key.Split(':', 2); + if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key)); + return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] }; + } + + public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}"; + public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId); + public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId); + + public override bool Equals(object? obj) => obj is VersionedKeyId other && Algorithm == other.Algorithm && KeyId == other.KeyId; + public override int GetHashCode() => HashCode.Combine(Algorithm, KeyId); +} \ No newline at end of file diff --git a/LibMatrix/Abstractions/VersionedPublicKey.cs b/LibMatrix/Abstractions/VersionedPublicKey.cs new file mode 100644 index 0000000..ad6747d --- /dev/null +++ b/LibMatrix/Abstractions/VersionedPublicKey.cs @@ -0,0 +1,16 @@ +namespace LibMatrix.Abstractions; + +public class VersionedPublicKey { + public required VersionedKeyId KeyId { get; set; } + public required string PublicKey { get; set; } +} + +public class VersionedPrivateKey : VersionedPublicKey { + public required string PrivateKey { get; set; } +} +public class VersionedHomeserverPublicKey : VersionedPublicKey { + public required string ServerName { get; set; } +} +public class VersionedHomeserverPrivateKey : VersionedPrivateKey { + public required string ServerName { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs index a2cb12d..9760e20 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs @@ -1,7 +1,6 @@ -using System.Text.Json.Serialization; using LibMatrix.Extensions; +using LibMatrix.Responses.Federation; using LibMatrix.Services; -using Microsoft.VisualBasic.CompilerServices; namespace LibMatrix.Homeservers; @@ -18,81 +17,7 @@ public class FederationClient { public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } public async Task GetServerVersionAsync() => await HttpClient.GetFromJsonAsync("/_matrix/federation/v1/version"); - public async Task GetServerKeysAsync() => await HttpClient.GetFromJsonAsync("/_matrix/key/v2/server"); + public async Task> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync>("/_matrix/key/v2/server"); } -public class ServerKeysResponse { - [JsonPropertyName("server_name")] - public string ServerName { get; set; } - [JsonPropertyName("valid_until_ts")] - public ulong ValidUntilTs { get; set; } - - [JsonIgnore] - public DateTime ValidUntil { - get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime; - set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); - } - - [JsonPropertyName("verify_keys")] - public Dictionary VerifyKeys { get; set; } = new(); - - [JsonIgnore] - public Dictionary VerifyKeysById { - get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); - set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); - } - - [JsonPropertyName("old_verify_keys")] - public Dictionary OldVerifyKeys { get; set; } = new(); - - [JsonIgnore] - public Dictionary OldVerifyKeysById { - get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); - set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); - } - - public class VersionedKeyId { - public required string Algorithm { get; set; } - public required string KeyId { get; set; } - - public static implicit operator VersionedKeyId(string key) { - var parts = key.Split(':', 2); - if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key)); - return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] }; - } - - public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}"; - public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId); - public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId); - } - - public class CurrentVerifyKey { - [JsonPropertyName("key")] - public string Key { get; set; } - } - - public class ExpiredVerifyKey : CurrentVerifyKey { - [JsonPropertyName("expired_ts")] - public ulong ExpiredTs { get; set; } - - [JsonIgnore] - public DateTime Expired { - get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime; - set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); - } - } -} - -public class ServerVersionResponse { - [JsonPropertyName("server")] - public required ServerInfo Server { get; set; } - - public class ServerInfo { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("version")] - public string Version { get; set; } - } -} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index 62bb48f..23d4eb8 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -18,8 +18,8 @@ - - + + diff --git a/LibMatrix/Responses/Federation/ServerKeysResponse.cs b/LibMatrix/Responses/Federation/ServerKeysResponse.cs new file mode 100644 index 0000000..cb62e34 --- /dev/null +++ b/LibMatrix/Responses/Federation/ServerKeysResponse.cs @@ -0,0 +1,55 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; +using LibMatrix.Abstractions; + +namespace LibMatrix.Responses.Federation; + +public class ServerKeysResponse { + [JsonPropertyName("server_name")] + public string ServerName { get; set; } + + [JsonPropertyName("valid_until_ts")] + public ulong ValidUntilTs { get; set; } + + [JsonIgnore] + public DateTime ValidUntil { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime; + set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [JsonPropertyName("verify_keys")] + public Dictionary VerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary VerifyKeysById { + get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + [JsonPropertyName("old_verify_keys")] + public Dictionary OldVerifyKeys { get; set; } = new(); + + [JsonIgnore] + public Dictionary OldVerifyKeysById { + get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value); + set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value); + } + + [DebuggerDisplay("{Key}")] + public class CurrentVerifyKey { + [JsonPropertyName("key")] + public string Key { get; set; } + } + + [DebuggerDisplay("{Key} (expired {Expired})")] + public class ExpiredVerifyKey : CurrentVerifyKey { + [JsonPropertyName("expired_ts")] + public ulong ExpiredTs { get; set; } + + [JsonIgnore] + public DateTime Expired { + get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime; + set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + } +} diff --git a/LibMatrix/Responses/Federation/ServerVersionResponse.cs b/LibMatrix/Responses/Federation/ServerVersionResponse.cs new file mode 100644 index 0000000..b09bdd0 --- /dev/null +++ b/LibMatrix/Responses/Federation/ServerVersionResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Responses.Federation; + +public class ServerVersionResponse { + [JsonPropertyName("server")] + public required ServerInfo Server { get; set; } + + public class ServerInfo { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Responses/Federation/SignedObject.cs b/LibMatrix/Responses/Federation/SignedObject.cs new file mode 100644 index 0000000..3f6ffd6 --- /dev/null +++ b/LibMatrix/Responses/Federation/SignedObject.cs @@ -0,0 +1,68 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; +using LibMatrix.Abstractions; +using LibMatrix.Homeservers; + +namespace LibMatrix.Responses.Federation; + +[JsonConverter(typeof(SignedObjectConverterFactory))] +public class SignedObject { + [JsonPropertyName("signatures")] + public Dictionary> Signatures { get; set; } = new(); + + [JsonIgnore] + public Dictionary> SignaturesById { + get => Signatures.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value)); + set => Signatures = value.ToDictionary(server => server.Key, server => server.Value.ToDictionary(key => (string)key.Key, key => key.Value)); + } + + [JsonExtensionData] + public required JsonObject Content { get; set; } + + [JsonIgnore] + public T TypedContent { + get => Content.Deserialize() ?? throw new JsonException("Failed to deserialize TypedContent from Content."); + set => Content = JsonSerializer.Deserialize(JsonSerializer.Serialize(value)) ?? new JsonObject(); + } +} + +public class SignedObjectConverter : JsonConverter> { + public override SignedObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + var jsonObject = JsonSerializer.Deserialize(ref reader, options); + if (jsonObject == null) { + throw new JsonException("Failed to deserialize SignedObject, JSON object is null."); + } + + var signatures = jsonObject["signatures"] ?? throw new JsonException("Failed to find 'signatures' property in JSON object."); + jsonObject.Remove("signatures"); + + var signedObject = new SignedObject { + Content = jsonObject, + Signatures = signatures.Deserialize>>() + ?? throw new JsonException("Failed to deserialize 'signatures' property into Dictionary>.") + }; + + return signedObject; + } + + public override void Write(Utf8JsonWriter writer, SignedObject value, JsonSerializerOptions options) { + var targetObj = value.Content.DeepClone(); + targetObj["signatures"] = value.Signatures.ToJsonNode(); + JsonSerializer.Serialize(writer, targetObj, options); + } +} + +internal class SignedObjectConverterFactory : JsonConverterFactory { + public override bool CanConvert(Type typeToConvert) { + if (!typeToConvert.IsGenericType) return false; + return typeToConvert.GetGenericTypeDefinition() == typeof(SignedObject<>); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { + var wrappedType = typeToConvert.GetGenericArguments()[0]; + var converter = (JsonConverter)Activator.CreateInstance(typeof(SignedObjectConverter<>).MakeGenericType(wrappedType))!; + return converter; + } +} \ No newline at end of file diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs index 36bc828..c984c34 100644 --- a/LibMatrix/Services/HomeserverProviderService.cs +++ b/LibMatrix/Services/HomeserverProviderService.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using ArcaneLibs.Collections; using LibMatrix.Homeservers; using LibMatrix.Responses; +using LibMatrix.Responses.Federation; using Microsoft.Extensions.Logging; namespace LibMatrix.Services; -- cgit 1.5.1