using System.Security.Cryptography; using System.Text; using Foundation; using Security; namespace VoiceCat.Mac; internal sealed class MacKeychainPasswordStore { private const string Service = "net.iamtalon.voicecat"; internal string? Load(Guid profileId) { using var query = Query(profileId); using SecRecord? result = SecKeyChain.QueryAsRecord(query, out SecStatusCode status); if (status != SecStatusCode.Success || result?.ValueData is not { } data) return null; byte[] encoded = data.ToArray(); try { return Encoding.UTF8.GetString(encoded); } finally { CryptographicOperations.ZeroMemory(encoded); } } internal string? LoadLegacy(string? keychainTag) { if (string.IsNullOrWhiteSpace(keychainTag)) return null; using var query = new SecRecord(SecKind.GenericPassword) { Service = "cat.voice.VoiceCatMac", Account = keychainTag }; using SecRecord? result = SecKeyChain.QueryAsRecord(query, out SecStatusCode status); if (status != SecStatusCode.Success || result?.ValueData is not { } data) return null; byte[] encoded = data.ToArray(); try { return Encoding.UTF8.GetString(encoded); } finally { CryptographicOperations.ZeroMemory(encoded); } } internal void Save(Guid profileId, string password) { ArgumentException.ThrowIfNullOrEmpty(password); byte[] encoded = Encoding.UTF8.GetBytes(password); try { using var data = NSData.FromArray(encoded); using var query = Query(profileId); using var attributes = new SecRecord { Label = "VoiceCat server password", Accessible = SecAccessible.WhenUnlocked, ValueData = data }; SecStatusCode status = SecKeyChain.Update(query, attributes); if (status == SecStatusCode.ItemNotFound) { using var record = Query(profileId); record.Label = attributes.Label; record.Accessible = attributes.Accessible; record.ValueData = data; status = SecKeyChain.Add(record); } if (status != SecStatusCode.Success) throw new InvalidOperationException($"Keychain save failed ({status})."); } finally { CryptographicOperations.ZeroMemory(encoded); } } internal void Remove(Guid profileId) { using var query = Query(profileId); SecStatusCode status = SecKeyChain.Remove(query); if (status is not (SecStatusCode.Success or SecStatusCode.ItemNotFound)) throw new InvalidOperationException($"Keychain removal failed ({status})."); } private static SecRecord Query(Guid profileId) => new(SecKind.GenericPassword) { Service = Service, Account = profileId.ToString("D") }; }