using System.Security.Cryptography; namespace AcDream.App.Credentials; /// /// Campaign LA slice LA1: retains a resolved --session-config /// credential in erasable memory — the App-side mirror of /// AcDream.Headless.Credentials.HeadlessCredentialSecret (that type is /// internal to the Headless project, so this is a minimal, independent port /// rather than a shared reference). The network boundary still requires one /// short-lived immutable string; callers must not retain that value beyond /// constructing the connect request. /// internal sealed class AppCredentialSecret : IDisposable { private char[]? _buffer; internal AppCredentialSecret(string referenceId, ReadOnlySpan value) { ArgumentException.ThrowIfNullOrWhiteSpace(referenceId); if (value.IsEmpty) { throw new AppCredentialException( $"Credential '{referenceId}' resolved to an empty secret."); } ReferenceId = referenceId; _buffer = value.ToArray(); } internal string ReferenceId { get; } internal bool IsDisposed => _buffer is null; internal string Reveal() { ObjectDisposedException.ThrowIf(_buffer is null, this); return new string(_buffer); } public void Dispose() { char[]? buffer = Interlocked.Exchange(ref _buffer, null); if (buffer is null) return; CryptographicOperations.ZeroMemory( System.Runtime.InteropServices.MemoryMarshal.AsBytes( buffer.AsSpan())); } public override string ToString() => $"[redacted:{ReferenceId}]"; } internal sealed class AppCredentialException : Exception { internal AppCredentialException(string message) : base(message) { } internal AppCredentialException(string message, Exception innerException) : base(message, innerException) { } }