using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DodoSSH.Client.Domain;
/// A decoded bucket payload, together with the schema version it was written at.
/// The bucket.
/// The version the writing client used.
public sealed record ObjectStoreSecretDocument(ObjectStoreSecret Store, int SchemaVersion)
{
///
public bool IsReadOnly => SchemaVersion > ObjectStoreSecretCodec.CurrentSchemaVersion;
}
///
/// Encodes and decodes the plaintext inside a bucket item's encrypted payload.
///
///
/// Mirrors , for the same reasons and with the same guarantees.
///
public static class ObjectStoreSecretCodec
{
/// The schema version this build writes.
public const int CurrentSchemaVersion = 1;
/// Serialises a bucket to the bytes that get sealed.
/// The bucket is not valid for storage.
public static byte[] Encode(ObjectStoreSecret store)
{
ArgumentNullException.ThrowIfNull(store);
if (!store.TryValidate(out var reason))
{
throw new ArgumentException(reason, nameof(store));
}
var document = new ObjectStorePayloadDocument
{
SchemaVersion = CurrentSchemaVersion,
Label = store.Label,
Bucket = store.Bucket,
AccessKeyId = store.AccessKeyId,
SecretAccessKey = store.SecretAccessKey,
Region = store.Region,
Endpoint = store.Endpoint,
UsePathStyle = store.UsePathStyle,
Notes = store.Notes,
};
return JsonSerializer.SerializeToUtf8Bytes(
document, ObjectStorePayloadJsonContext.Default.ObjectStorePayloadDocument);
}
/// Parses a decrypted payload.
///
public static bool TryDecode(
ReadOnlySpan payload,
[NotNullWhen(true)] out ObjectStoreSecretDocument? document)
{
document = null;
ObjectStorePayloadDocument? parsed;
try
{
parsed = JsonSerializer.Deserialize(
payload, ObjectStorePayloadJsonContext.Default.ObjectStorePayloadDocument);
}
catch (JsonException)
{
return false;
}
if (parsed is null || parsed.SchemaVersion < 1)
{
return false;
}
var candidate = new ObjectStoreSecret
{
Label = parsed.Label ?? string.Empty,
Bucket = parsed.Bucket ?? string.Empty,
AccessKeyId = parsed.AccessKeyId ?? string.Empty,
SecretAccessKey = parsed.SecretAccessKey ?? string.Empty,
Region = parsed.Region,
Endpoint = parsed.Endpoint,
UsePathStyle = parsed.UsePathStyle,
Notes = parsed.Notes,
};
if (!candidate.TryValidate(out _))
{
return false;
}
document = new ObjectStoreSecretDocument(candidate, parsed.SchemaVersion);
return true;
}
}
/// The serialised shape. Mutable and nullable because it models untrusted input.
///
internal sealed class ObjectStorePayloadDocument
{
public int SchemaVersion { get; set; }
public string? Label { get; set; }
public string? Bucket { get; set; }
public string? AccessKeyId { get; set; }
public string? SecretAccessKey { get; set; }
public string? Region { get; set; }
public string? Endpoint { get; set; }
public bool UsePathStyle { get; set; }
public string? Notes { get; set; }
}
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip)]
[JsonSerializable(typeof(ObjectStorePayloadDocument))]
internal sealed partial class ObjectStorePayloadJsonContext : JsonSerializerContext;