Files

73 lines
2.8 KiB
C#
Raw Permalink Normal View History

2026-09-16 22:22:03 +02:00
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); }
}
2026-09-16 22:22:03 +02:00
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")
};
}