// Copyright (C) 2026, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Web;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
namespace Duplicati.Library.SecretProvider;
///
/// Implementation of a secret provider that reads secrets from the MacOS keychain
///
[SupportedOSPlatform("macos")]
public class MacOSKeyChainProvider : ISecretProvider
{
///
public string Key => "keychain";
///
public string DisplayName => Strings.MacOSKeyChainProvider.DisplayName;
///
public string Description => Strings.MacOSKeyChainProvider.Description;
///
public Task IsSupported(CancellationToken cancellationToken) => Task.FromResult(OperatingSystem.IsMacOS());
///
public bool IsSetSupported => true;
///
/// The type of password to get
///
private enum PasswordType
{
///
/// A generic password
///
Generic,
///
/// An internet password
///
Internet
}
///
/// The settings for the keychain
///
private class KeyChainSettings : ICommandLineArgumentMapper
{
///
/// The default service name
///
private const string ServiceDefault = "com.duplicati.secrets";
///
/// The default account name
///
private const string AccountDefault = "Duplicati";
///
/// The service name
///
public string? Service { get; set; } = ServiceDefault;
///
/// The account name
///
public string? Account { get; set; } = AccountDefault;
///
/// Gets or sets a value indicating whether to use internet passwords as opposed to generic passwords
///
public PasswordType Type { get; set; } = PasswordType.Generic;
///
/// Gets the command line argument description for a member
///
/// The name of the member
/// The command line argument description
public static CommandLineArgumentDescriptionAttribute? GetCommandLineArgumentDescription(string name)
=> name switch
{
nameof(Service) => new CommandLineArgumentDescriptionAttribute
{
Name = "service",
ShortDescription = Strings.MacOSKeyChainProvider.ServiceDescriptionShort,
LongDescription = Strings.MacOSKeyChainProvider.ServiceDescriptionLong,
DefaultValue = ServiceDefault
},
nameof(Account) => new CommandLineArgumentDescriptionAttribute
{
Name = "account",
ShortDescription = Strings.MacOSKeyChainProvider.AccountDescriptionShort,
LongDescription = Strings.MacOSKeyChainProvider.AccountDescriptionLong,
DefaultValue = AccountDefault
},
nameof(Type) => new CommandLineArgumentDescriptionAttribute
{
Name = "type",
Type = CommandLineArgument.ArgumentType.Enumeration,
ShortDescription = Strings.MacOSKeyChainProvider.TypeDescriptionShort,
LongDescription = Strings.MacOSKeyChainProvider.TypeDescriptionLong
},
_ => null
};
///
CommandLineArgumentDescriptionAttribute? ICommandLineArgumentMapper.GetCommandLineArgumentDescription(MemberInfo mi)
=> GetCommandLineArgumentDescription(mi.Name);
}
///
/// The settings for the keychain; null if not initialized
///
private KeyChainSettings? _settings;
///
public IList SupportedCommands
=> CommandLineArgumentMapper.MapArguments(new KeyChainSettings()).ToList();
///
public Task InitializeAsync(System.Uri config, CancellationToken cancellationToken)
{
if (!OperatingSystem.IsMacOS())
throw new PlatformNotSupportedException("The MacOSKeyChainProvider is only supported on macOS");
var args = HttpUtility.ParseQueryString(config.Query);
_settings = CommandLineArgumentMapper.ApplyArguments(new KeyChainSettings(), args);
return Task.CompletedTask;
}
///
public async Task> ResolveSecretsAsync(IEnumerable keys, CancellationToken cancellationToken)
{
if (_settings is null)
throw new InvalidOperationException("The MacOSKeyChainProvider has not been initialized");
var result = new Dictionary();
foreach (var key in keys)
{
cancellationToken.ThrowIfCancellationRequested();
var value = await GetStringAsync(key, _settings, cancellationToken).ConfigureAwait(false);
result[key] = value;
}
return result;
}
///
public async Task SetSecretAsync(string key, string value, bool overwrite, CancellationToken cancellationToken)
{
if (!OperatingSystem.IsMacOS())
throw new PlatformNotSupportedException("The MacOSKeyChainProvider is only supported on MacOS");
if (_settings is null)
throw new InvalidOperationException("The MacOSKeyChainProvider has not been initialized");
cancellationToken.ThrowIfCancellationRequested();
await SetStringAsync(key, value, overwrite, _settings, cancellationToken).ConfigureAwait(false);
}
///
/// Native methods for accessing the MacOS keychain
///
private static class KeychainNative
{
///
/// The path to the Security framework
///
private const string SecurityLib = "/System/Library/Frameworks/Security.framework/Security";
///
/// The path to the CoreFoundation framework
///
private const string CoreFoundationLib = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
///
/// Success status code
///
public const int errSecSuccess = 0;
///
/// Duplicate item status code
///
public const int errSecDuplicateItem = -25299;
// Classic exact-match APIs (fast path)
///
/// Finds a generic password item in the keychain.
///
/// The keychain to search, or null for default.
/// Length of the service name.
/// The service name.
/// Length of the account name.
/// The account name.
/// Output: length of the password data.
/// Output: pointer to the password data.
/// Output: reference to the keychain item.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecKeychainFindGenericPassword(
IntPtr keychain,
uint serviceNameLength,
byte[] serviceName,
uint accountNameLength,
byte[] accountName,
out uint passwordLength,
out IntPtr passwordData,
out IntPtr itemRef);
///
/// Finds an internet password item in the keychain.
///
/// The keychain to search, or null for default.
/// Length of the server name.
/// The server name.
/// Length of the security domain.
/// The security domain.
/// Length of the account name.
/// The account name.
/// Length of the path.
/// The path.
/// The port number.
/// The protocol type.
/// The authentication type.
/// Output: length of the password data.
/// Output: pointer to the password data.
/// Output: reference to the keychain item.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecKeychainFindInternetPassword(
IntPtr keychain,
uint serverNameLength,
byte[] serverName,
uint securityDomainLength,
byte[] securityDomain,
uint accountNameLength,
byte[] accountName,
uint pathLength,
byte[] path,
ushort port,
int protocol,
int authType,
out uint passwordLength,
out IntPtr passwordData,
out IntPtr itemRef);
///
/// Modifies the attributes and data of a keychain item.
///
/// Reference to the keychain item.
/// List of attributes to modify, or null.
/// Length of the data.
/// The new data.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecKeychainItemModifyAttributesAndData(
IntPtr itemRef,
IntPtr attrList,
uint length,
byte[] data);
///
/// Frees the memory allocated for keychain item content.
///
/// The attribute list to free, or null.
/// The data to free.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecKeychainItemFreeContent(IntPtr attrList, IntPtr data);
// Modern SecItem* APIs (label-aware add/update + label-only lookup)
///
/// Copies matching items from the keychain.
///
/// Dictionary containing the query parameters.
/// Output: the matching item or items.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecItemCopyMatching(IntPtr query, out IntPtr result);
///
/// Adds an item to the keychain.
///
/// Dictionary containing the item attributes.
/// Output: reference to the added item.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecItemAdd(IntPtr attributes, out IntPtr result);
///
/// Updates an item in the keychain.
///
/// Dictionary identifying the item to update.
/// Dictionary containing the attributes to update.
/// Status code; 0 for success.
[DllImport(SecurityLib)]
internal static extern int SecItemUpdate(IntPtr query, IntPtr attributesToUpdate);
// CF helpers
///
/// Creates a mutable CoreFoundation dictionary.
///
/// The allocator to use, or null for default.
/// Initial capacity of the dictionary.
/// Callbacks for key operations.
/// Callbacks for value operations.
/// Pointer to the created dictionary.
[DllImport(CoreFoundationLib)]
internal static extern IntPtr CFDictionaryCreateMutable(
IntPtr allocator,
nint capacity,
IntPtr keyCallBacks,
IntPtr valueCallBacks);
///
/// Sets a value in a CoreFoundation dictionary.
///
/// The dictionary.
/// The key.
/// The value.
[DllImport(CoreFoundationLib)]
internal static extern void CFDictionarySetValue(IntPtr dict, IntPtr key, IntPtr value);
///
/// Creates a CoreFoundation string from a C string.
///
/// The allocator to use, or null for default.
/// The C string bytes.
/// The string encoding.
/// Pointer to the created string.
[DllImport(CoreFoundationLib)]
internal static extern IntPtr CFStringCreateWithCString(
IntPtr alloc,
byte[] cStr,
uint encoding);
///
/// Creates CoreFoundation data from a byte array.
///
/// The allocator to use, or null for default.
/// The byte array.
/// The length of the data.
/// Pointer to the created data.
[DllImport(CoreFoundationLib)]
internal static extern IntPtr CFDataCreate(
IntPtr allocator,
byte[] bytes,
nint length);
///
/// Gets the length of CoreFoundation data.
///
/// The data object.
/// The length of the data.
[DllImport(CoreFoundationLib)]
internal static extern nint CFDataGetLength(IntPtr data);
///
/// Gets a pointer to the bytes of CoreFoundation data.
///
/// The data object.
/// Pointer to the byte data.
[DllImport(CoreFoundationLib)]
internal static extern IntPtr CFDataGetBytePtr(IntPtr data);
///
/// Releases a CoreFoundation object.
///
/// The object to release.
[DllImport(CoreFoundationLib)]
internal static extern void CFRelease(IntPtr cf);
///
/// UTF-8 encoding constant for CoreFoundation strings.
///
internal const uint kCFStringEncodingUTF8 = 0x08000100;
// Constant pointers exported by Security/CoreFoundation, loaded via dlsym
///
/// Path to libSystem (for dlopen/dlsym).
///
private const string LibSystem = "/usr/lib/libSystem.B.dylib";
///
/// Loads a dynamic library.
///
/// The path to the library.
/// The loading mode.
/// Handle to the loaded library.
[DllImport(LibSystem, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr dlopen(string path, int mode);
///
/// Resolves a symbol from a dynamic library.
///
/// Handle to the loaded library.
/// The symbol to resolve.
/// Pointer to the resolved symbol.
[DllImport(LibSystem, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
///
/// Closes a dynamic library.
///
/// Handle to the loaded library.
/// Zero on success.
[DllImport(LibSystem, CallingConvention = CallingConvention.Cdecl)]
private static extern int dlclose(IntPtr handle);
private static readonly IntPtr _securityHandle;
private static readonly IntPtr _coreFoundationHandle;
///
/// Key for the class attribute in keychain queries.
///
internal static readonly IntPtr SecClass;
///
/// Value for generic password class.
///
internal static readonly IntPtr SecClassGenericPassword;
///
/// Value for internet password class.
///
internal static readonly IntPtr SecClassInternetPassword;
///
/// Key for the service attribute.
///
internal static readonly IntPtr SecAttrService;
///
/// Key for the server attribute.
///
internal static readonly IntPtr SecAttrServer;
///
/// Key for the account attribute.
///
internal static readonly IntPtr SecAttrAccount;
///
/// Key for the label attribute.
///
internal static readonly IntPtr SecAttrLabel;
///
/// Key for the value data.
///
internal static readonly IntPtr SecValueData;
///
/// Key to specify returning data in queries.
///
internal static readonly IntPtr SecReturnData;
///
/// Key for match limit in queries.
///
internal static readonly IntPtr SecMatchLimit;
///
/// Value for limiting matches to one.
///
internal static readonly IntPtr SecMatchLimitOne;
///
/// CoreFoundation true boolean value.
///
internal static readonly IntPtr CFBooleanTrue;
///
/// Static constructor to load native libraries and resolve constant symbols.
///
static KeychainNative()
{
const int RTLD_LAZY = 0x1;
_securityHandle = dlopen(SecurityLib, RTLD_LAZY);
_coreFoundationHandle = dlopen(CoreFoundationLib, RTLD_LAZY);
if (_securityHandle == IntPtr.Zero || _coreFoundationHandle == IntPtr.Zero)
throw new Exception("Failed to load native Security/CoreFoundation frameworks");
SecClass = GetSymbol(_securityHandle, "kSecClass");
SecClassGenericPassword = GetSymbol(_securityHandle, "kSecClassGenericPassword");
SecClassInternetPassword = GetSymbol(_securityHandle, "kSecClassInternetPassword");
SecAttrService = GetSymbol(_securityHandle, "kSecAttrService");
SecAttrServer = GetSymbol(_securityHandle, "kSecAttrServer");
SecAttrAccount = GetSymbol(_securityHandle, "kSecAttrAccount");
SecAttrLabel = GetSymbol(_securityHandle, "kSecAttrLabel");
SecValueData = GetSymbol(_securityHandle, "kSecValueData");
SecReturnData = GetSymbol(_securityHandle, "kSecReturnData");
SecMatchLimit = GetSymbol(_securityHandle, "kSecMatchLimit");
SecMatchLimitOne = GetSymbol(_securityHandle, "kSecMatchLimitOne");
CFBooleanTrue = GetSymbol(_coreFoundationHandle, "kCFBooleanTrue");
}
///
/// Resolves a symbol and reads its pointer value.
///
/// Handle to the loaded library.
/// The symbol to resolve.
/// Pointer to the resolved symbol.
private static IntPtr GetSymbol(IntPtr handle, string name)
{
var symbolPtr = dlsym(handle, name);
if (symbolPtr == IntPtr.Zero)
throw new Exception($"Failed to resolve native symbol '{name}'");
var value = Marshal.ReadIntPtr(symbolPtr);
if (value == IntPtr.Zero)
throw new Exception($"Native symbol '{name}' is null");
return value;
}
///
/// Creates a CoreFoundation string from a managed string.
///
/// The input string.
/// Pointer to the CFString.
internal static IntPtr CFString(string s)
{
var bytes = Encoding.UTF8.GetBytes(s + "\0");
var cf = CFStringCreateWithCString(IntPtr.Zero, bytes, kCFStringEncodingUTF8);
if (cf == IntPtr.Zero)
throw new Exception("CFStringCreateWithCString failed");
return cf;
}
///
/// Creates CoreFoundation data from a byte array.
///
/// The byte array.
/// Pointer to the CFData.
internal static IntPtr CFData(byte[] bytes)
{
var cf = CFDataCreate(IntPtr.Zero, bytes, bytes.Length);
if (cf == IntPtr.Zero)
throw new Exception("CFDataCreate failed");
return cf;
}
///
/// Creates a new mutable CoreFoundation dictionary.
///
/// Pointer to the dictionary.
internal static IntPtr NewMutableDict()
{
var dict = CFDictionaryCreateMutable(
IntPtr.Zero,
0,
IntPtr.Zero,
IntPtr.Zero);
if (dict == IntPtr.Zero)
throw new Exception("CFDictionaryCreateMutable failed");
return dict;
}
///
/// Sets a value in a CoreFoundation dictionary.
///
/// The dictionary.
/// The key.
/// The value.
internal static void DictSet(IntPtr dict, IntPtr key, IntPtr value)
=> CFDictionarySetValue(dict, key, value);
}
///
/// Stores a string value in the keychain asynchronously.
///
/// The name/key for the secret.
/// The secret value to store.
/// Whether to overwrite an existing item.
/// The keychain settings.
/// Cancellation token.
private static Task SetStringAsync(string name, string secret, bool overwrite, KeyChainSettings settings, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
SetItem(name, secret, overwrite, settings, isInternet: settings.Type == PasswordType.Internet);
return Task.CompletedTask;
}
///
/// Returns the service name to use for the given name and settings.
///
/// The name of the secret.
/// The keychain settings.
/// The service name to use.
private static string GetServiceName(string name, KeyChainSettings settings)
{
if (string.IsNullOrWhiteSpace(settings.Service))
return name;
return $"{settings.Service}.{name}";
}
///
/// Returns the account name to use for the given name and settings.
///
/// The name of the secret.
/// The keychain settings.
/// The account name to use.
private static string GetAccountName(string name, KeyChainSettings settings)
{
if (string.IsNullOrWhiteSpace(settings.Account))
return name;
return settings.Account;
}
///
/// Stores a generic password in the keychain.
///
/// The label for the item.
/// The secret value.
/// Whether to overwrite if exists.
/// The keychain settings.
private static void SetItem(string label, string secret, bool overwrite, KeyChainSettings settings, bool isInternet)
{
var serviceOrServer = GetServiceName(label, settings);
var account = GetAccountName(label, settings);
IntPtr attrs = IntPtr.Zero, q = IntPtr.Zero, upd = IntPtr.Zero;
IntPtr cfServiceOrServer = IntPtr.Zero, cfAccount = IntPtr.Zero, cfLabel = IntPtr.Zero, cfSecret = IntPtr.Zero;
try
{
cfServiceOrServer = KeychainNative.CFString(serviceOrServer);
cfAccount = KeychainNative.CFString(account);
cfLabel = KeychainNative.CFString(label);
cfSecret = KeychainNative.CFData(Encoding.UTF8.GetBytes(secret));
// Build attributes for add
attrs = KeychainNative.NewMutableDict();
KeychainNative.DictSet(attrs, KeychainNative.SecClass,
isInternet ? KeychainNative.SecClassInternetPassword
: KeychainNative.SecClassGenericPassword);
if (isInternet)
KeychainNative.DictSet(attrs, KeychainNative.SecAttrServer, cfServiceOrServer);
else
KeychainNative.DictSet(attrs, KeychainNative.SecAttrService, cfServiceOrServer);
KeychainNative.DictSet(attrs, KeychainNative.SecAttrAccount, cfAccount);
KeychainNative.DictSet(attrs, KeychainNative.SecAttrLabel, cfLabel);
KeychainNative.DictSet(attrs, KeychainNative.SecValueData, cfSecret);
var status = KeychainNative.SecItemAdd(attrs, out var added);
if (added != IntPtr.Zero) KeychainNative.CFRelease(added);
if (status == KeychainNative.errSecSuccess)
return;
if (status != KeychainNative.errSecDuplicateItem)
throw new UserInformationException(
$"Failed to store secret in keychain (status {status})",
"KeyChainInsertFailed");
// Duplicate item
if (!overwrite)
throw new UserInformationException(
$"Item already exists in keychain: {label}",
"KeyChainInsertFailed");
// Query for the existing item to update (exact key)
q = KeychainNative.NewMutableDict();
KeychainNative.DictSet(q, KeychainNative.SecClass,
isInternet ? KeychainNative.SecClassInternetPassword
: KeychainNative.SecClassGenericPassword);
if (isInternet)
KeychainNative.DictSet(q, KeychainNative.SecAttrServer, cfServiceOrServer);
else
KeychainNative.DictSet(q, KeychainNative.SecAttrService, cfServiceOrServer);
KeychainNative.DictSet(q, KeychainNative.SecAttrAccount, cfAccount);
// Update dictionary
upd = KeychainNative.NewMutableDict();
KeychainNative.DictSet(upd, KeychainNative.SecValueData, cfSecret);
KeychainNative.DictSet(upd, KeychainNative.SecAttrLabel, cfLabel);
status = KeychainNative.SecItemUpdate(q, upd);
if (status != KeychainNative.errSecSuccess)
throw new UserInformationException(
$"Failed to update secret in keychain (status {status})",
"KeyChainInsertFailed");
}
finally
{
if (attrs != IntPtr.Zero) KeychainNative.CFRelease(attrs);
if (q != IntPtr.Zero) KeychainNative.CFRelease(q);
if (upd != IntPtr.Zero) KeychainNative.CFRelease(upd);
if (cfServiceOrServer != IntPtr.Zero) KeychainNative.CFRelease(cfServiceOrServer);
if (cfAccount != IntPtr.Zero) KeychainNative.CFRelease(cfAccount);
if (cfLabel != IntPtr.Zero) KeychainNative.CFRelease(cfLabel);
if (cfSecret != IntPtr.Zero) KeychainNative.CFRelease(cfSecret);
}
}
///
/// Retrieves a string value from the keychain asynchronously.
///
/// The name/key of the secret.
/// The keychain settings.
/// Cancellation token.
/// The retrieved secret value.
private static Task GetStringAsync(string name, KeyChainSettings settings, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
// If service/account is set (either one), try exact match first.
if (!string.IsNullOrEmpty(settings.Service) || !string.IsNullOrEmpty(settings.Account))
{
try
{
return Task.FromResult(GetByLabelCore(name, settings, settings.Type == PasswordType.Internet));
}
catch (UserInformationException ex) when (ex.HelpID == "KeyChainItemMissing")
{
// Fallback to label-only below.
}
}
return Task.FromResult(GetByLabelCore(name, null, settings.Type == PasswordType.Internet));
}
///
/// Core method to retrieve a password by label.
///
/// The label of the item.
/// Whether it's an internet password.
/// The password value.
private static string GetByLabelCore(string name, KeyChainSettings? settings, bool isInternet)
{
IntPtr q = IntPtr.Zero;
IntPtr cfLabel = IntPtr.Zero;
IntPtr cfService = IntPtr.Zero;
IntPtr cfAccount = IntPtr.Zero;
IntPtr result = IntPtr.Zero;
try
{
cfLabel = KeychainNative.CFString(name);
q = KeychainNative.NewMutableDict();
KeychainNative.DictSet(q, KeychainNative.SecClass,
isInternet ? KeychainNative.SecClassInternetPassword
: KeychainNative.SecClassGenericPassword);
// Always constrain by label/name
KeychainNative.DictSet(q, KeychainNative.SecAttrLabel, cfLabel);
// Optionally constrain by service + account as well
if (settings != null && (!string.IsNullOrWhiteSpace(settings.Service) || !string.IsNullOrWhiteSpace(settings.Account)))
{
var service = GetServiceName(name, settings);
var account = GetAccountName(name, settings);
cfService = KeychainNative.CFString(service);
cfAccount = KeychainNative.CFString(account);
KeychainNative.DictSet(q, KeychainNative.SecAttrService, cfService);
KeychainNative.DictSet(q, KeychainNative.SecAttrAccount, cfAccount);
}
KeychainNative.DictSet(q, KeychainNative.SecReturnData, KeychainNative.CFBooleanTrue);
KeychainNative.DictSet(q, KeychainNative.SecMatchLimit, KeychainNative.SecMatchLimitOne);
var status = KeychainNative.SecItemCopyMatching(q, out result);
if (status != 0 || result == IntPtr.Zero)
{
var msg = settings != null
? $"Item not found in keychain: label={name}, service={GetServiceName(name, settings)}, account={GetAccountName(name, settings)}"
: $"Item not found in keychain by label: {name}";
throw new UserInformationException(msg, "KeyChainItemMissing");
}
var len = (int)KeychainNative.CFDataGetLength(result);
if (len <= 0)
throw new UserInformationException($"The key '{name}' returned an empty value", "KeyChainItemEmpty");
var ptr = KeychainNative.CFDataGetBytePtr(result);
var managed = new byte[len];
Marshal.Copy(ptr, managed, 0, len);
var output = Encoding.UTF8.GetString(managed).Trim();
ValidateOutput(name, output);
return output;
}
finally
{
if (result != IntPtr.Zero) KeychainNative.CFRelease(result);
if (q != IntPtr.Zero) KeychainNative.CFRelease(q);
if (cfLabel != IntPtr.Zero) KeychainNative.CFRelease(cfLabel);
if (cfService != IntPtr.Zero) KeychainNative.CFRelease(cfService);
if (cfAccount != IntPtr.Zero) KeychainNative.CFRelease(cfAccount);
}
}
///
/// Validates the retrieved output value.
///
/// The name/key for error messages.
/// The output string to validate.
private static void ValidateOutput(string name, string output)
{
if (string.IsNullOrWhiteSpace(output))
throw new UserInformationException($"The key '{name}' returned an empty value", "KeyChainItemEmpty");
if (output.IndexOfAny(['\r', '\n']) >= 0)
throw new UserInformationException($"The key '{name}' returned a multi-line value", "KeyChainItemMultiLine");
}
}