.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
62 lines
2.2 KiB
C#
62 lines
2.2 KiB
C#
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")
|
|
};
|
|
}
|