Files
scadaproj/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/MasterKeySnapshot.cs
T
Joseph Doherty b64628de0b fix(secrets): malformed rows fail as SecretDecryptionException; one KEK snapshot per cipher op (scadaproj#45)
Secrets-004: AesGcmEnvelopeCipher.Decrypt now wraps the ArgumentException AesGcm throws for a
wrong-length nonce/tag/wrapped DEK as SecretDecryptionException (as Rewrap already did), so the
resolver's decryption-failed audit fires.

Secrets-007: new MasterKeySnapshot (key + KekId from one reading, zeroed on Dispose) and a
default-implemented IMasterKeyProvider.GetSnapshot(); MasterKeyProviderBase overrides it to
resolve the source once and derive the id from that same key. Encrypt/Decrypt/Rewrap take one
snapshot per KEK and dispose it, so a key file swapped mid-operation can no longer stamp a row
with one key's id and wrap it under another. Intermediate key buffers are zeroed. On-disk format
unchanged: a golden row written by the 0.6.5 cipher still decrypts.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:32:28 -05:00

42 lines
1.8 KiB
C#

using System.Security.Cryptography;
namespace ZB.MOM.WW.Secrets.Abstractions;
/// <summary>
/// One consistent reading of a master key (KEK): the 32 key bytes together with the
/// <see cref="IMasterKeyProvider.KekId"/> that identifies <i>those</i> bytes. Obtained from
/// <see cref="IMasterKeyProvider.GetSnapshot"/> once per cipher operation, so the id stamped on a
/// row and the key that wraps it can never come from two different reads of a source that changed
/// in between (a key file replaced mid-operation).
/// </summary>
/// <remarks>
/// The snapshot owns its key buffer: <see cref="Dispose"/> zeroes it with
/// <see cref="CryptographicOperations.ZeroMemory"/>. Always dispose a snapshot
/// (<c>using var snapshot = provider.GetSnapshot();</c>) and never retain <see cref="Key"/> past it.
/// </remarks>
public sealed class MasterKeySnapshot : IDisposable
{
private readonly byte[] _key;
/// <summary>
/// Creates a snapshot that takes ownership of <paramref name="key"/> (it is zeroed on
/// <see cref="Dispose"/>; pass a buffer nothing else uses).
/// </summary>
/// <param name="kekId">The non-secret id of <paramref name="key"/>.</param>
/// <param name="key">The key bytes; ownership transfers to the snapshot.</param>
public MasterKeySnapshot(string kekId, byte[] key)
{
KekId = kekId ?? throw new ArgumentNullException(nameof(kekId));
_key = key ?? throw new ArgumentNullException(nameof(key));
}
/// <summary>The non-secret identifier of <see cref="Key"/>.</summary>
public string KekId { get; }
/// <summary>The key bytes. Valid only until <see cref="Dispose"/>, which zeroes them.</summary>
public ReadOnlyMemory<byte> Key => _key;
/// <summary>Zeroes the key buffer.</summary>
public void Dispose() => CryptographicOperations.ZeroMemory(_key);
}