Store macOS account passwords in Keychain
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
.NET port / cpp-conformance (push) Canceled after 0s

This commit is contained in:
2026-09-16 22:22:03 +02:00
parent 5a30018aeb
commit 28f1dbb991
5 changed files with 120 additions and 16 deletions
@@ -0,0 +1,61 @@
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 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")
};
}