using System.Security.Cryptography;
using DodoSSH.Client.Domain;
using DodoSSH.Contracts;
using DodoSSH.Crypto;
namespace DodoSSH.Client.Sync;
///
/// Turns a log entry into an item payload and back.
///
///
///
/// Mirrors exactly, including the rule that a payload is sealed at the
/// version the server will assign rather than the one it replaces — see .
/// In practice a log entry is only ever sealed at version 1, because nothing updates one; the general rule
/// is used anyway, so that this cipher does not become the one place where a different one applies.
///
///
/// The resource type is the one thing not to copy. SyncEntityType.ActivityLogEntry and
/// CryptoSpec.AadResourceType.ActivityLogEntry deliberately differ, as every pair in this folder does, and the
/// two log kinds sit next to each other in both enums — so a copied cipher with one constant left behind
/// seals a connection record under the resource type for an activity record. That encrypts perfectly,
/// decrypts perfectly on the machine that wrote it, and violates docs/crypto.md everywhere else.
///
///
public static class ActivityLogCipher
{
private const CryptoSpec.AadResourceType Resource = CryptoSpec.AadResourceType.ActivityLogEntry;
/// Encrypts an entry.
/// The entry. Must be valid for storage.
/// The vault key, which the data key is wrapped under.
/// The item id, which the AAD binds.
/// The vault's current key generation.
/// The version this payload will hold once the server accepts it.
public static EncryptedPayload Seal(
ActivityLogSecret entry,
ReadOnlySpan vaultKey,
Guid entityId,
uint keyGeneration,
int itemVersion)
{
ArgumentNullException.ThrowIfNull(entry);
ArgumentOutOfRangeException.ThrowIfLessThan(itemVersion, 1);
var plaintext = ActivityLogSecretCodec.Encode(entry);
var dataKey = ItemKeys.CreateDataKey();
try
{
var dataKeyId = Guid.CreateVersion7();
var wrappedDataKey = ItemKeys.WrapDataKey(
dataKey, vaultKey, Resource, entityId, keyGeneration, (uint)itemVersion);
var envelope = ItemKeys.SealPayload(
dataKey, plaintext, Resource, entityId, dataKeyId, keyGeneration, (uint)itemVersion);
return new EncryptedPayload(
envelope, wrappedDataKey, dataKeyId, keyGeneration, CryptoSpec.CurrentAadVersion);
}
finally
{
CryptographicOperations.ZeroMemory(dataKey);
// Wiped like every other payload here. Nothing in a log entry is a credential, and what it does
// hold — which machines this person reaches, and when — is the aggregate the whole item is
// encrypted to keep, so leaving it in a pooled buffer would be an odd place to stop caring.
CryptographicOperations.ZeroMemory(plaintext);
}
}
/// Decrypts an entry.
///
public static ActivityLogSecretDocument? TryOpen(
EncryptedPayload payload,
ReadOnlySpan vaultKey,
Guid entityId,
int itemVersion)
{
ArgumentNullException.ThrowIfNull(payload);
if (itemVersion < 1 || payload.WrappedDataKey.Length == 0)
{
return null;
}
var dataKey = ItemKeys.TryUnwrapDataKey(
vaultKey,
payload.WrappedDataKey,
Resource,
entityId,
payload.KeyGeneration,
(uint)itemVersion);
if (dataKey is null)
{
return null;
}
try
{
var plaintext = ItemKeys.TryOpenPayload(
dataKey,
payload.Envelope,
Resource,
entityId,
payload.DataKeyId,
payload.KeyGeneration,
(uint)itemVersion);
if (plaintext is null)
{
return null;
}
try
{
return ActivityLogSecretCodec.TryDecode(plaintext, out var document) ? document : null;
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
finally
{
CryptographicOperations.ZeroMemory(dataKey);
}
}
}