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); } } }