using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DodoSSH.Client.Domain;
/// A decoded Spelunk key payload, together with the schema version it was written at.
/// The key.
/// The version the writing client used.
public sealed record SpelunkKeySecretDocument(SpelunkKeySecret Key, int SchemaVersion)
{
///
public bool IsReadOnly => SchemaVersion > SpelunkKeySecretCodec.CurrentSchemaVersion;
}
///
/// Encodes and decodes the plaintext inside a Spelunk key item's encrypted payload.
///
///
/// Mirrors , for the same reasons and with the same guarantees.
///
public static class SpelunkKeySecretCodec
{
/// The schema version this build writes.
public const int CurrentSchemaVersion = 1;
/// Serialises a key to the bytes that get sealed.
/// The key is not valid for storage.
public static byte[] Encode(SpelunkKeySecret key)
{
ArgumentNullException.ThrowIfNull(key);
if (!key.TryValidate(out var reason))
{
throw new ArgumentException(reason, nameof(key));
}
var document = new SpelunkKeyPayloadDocument
{
SchemaVersion = CurrentSchemaVersion,
Label = key.Label,
ApiBase = key.ApiBase,
ApiKey = key.ApiKey,
};
return JsonSerializer.SerializeToUtf8Bytes(
document, SpelunkKeyPayloadJsonContext.Default.SpelunkKeyPayloadDocument);
}
/// Parses a decrypted payload.
///
public static bool TryDecode(
ReadOnlySpan payload,
[NotNullWhen(true)] out SpelunkKeySecretDocument? document)
{
document = null;
SpelunkKeyPayloadDocument? parsed;
try
{
parsed = JsonSerializer.Deserialize(
payload, SpelunkKeyPayloadJsonContext.Default.SpelunkKeyPayloadDocument);
}
catch (JsonException)
{
return false;
}
if (parsed is null || parsed.SchemaVersion < 1)
{
return false;
}
var candidate = new SpelunkKeySecret
{
Label = parsed.Label ?? string.Empty,
ApiBase = parsed.ApiBase ?? string.Empty,
ApiKey = parsed.ApiKey ?? string.Empty,
};
// A key whose address is not in its normal form is refused rather than normalised here: the address
// is what decides where the key may go, and a payload that disagrees with how every client writes it
// is not one to guess about.
if (!candidate.TryValidate(out _))
{
return false;
}
document = new SpelunkKeySecretDocument(candidate, parsed.SchemaVersion);
return true;
}
}
/// The serialised shape. Mutable and nullable because it models untrusted input.
///
internal sealed class SpelunkKeyPayloadDocument
{
public int SchemaVersion { get; set; }
public string? Label { get; set; }
public string? ApiBase { get; set; }
public string? ApiKey { get; set; }
}
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip)]
[JsonSerializable(typeof(SpelunkKeyPayloadDocument))]
internal sealed partial class SpelunkKeyPayloadJsonContext : JsonSerializerContext;