Skip to content

Commit d69b783

Browse files
committed
refactor: rewrite macOS keychain integration since SecKeychain* methods are marked as Deprecated
Signed-off-by: leo <longshuang@msn.cn>
1 parent 7ae0193 commit d69b783

1 file changed

Lines changed: 163 additions & 107 deletions

File tree

‎src/Native/MacOS.cs‎

Lines changed: 163 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -349,140 +349,196 @@ public static void ShowAllApplications()
349349
[SupportedOSPlatform("macOS")]
350350
public class MacOSKeychain : OS.IKeychain
351351
{
352-
[DllImport("/System/Library/Frameworks/Security.framework/Security")]
353-
private static extern int SecKeychainAddGenericPassword(
354-
IntPtr keychain,
355-
uint serviceNameLength,
356-
byte[] serviceName,
357-
uint accountNameLength,
358-
byte[] accountName,
359-
uint passwordLength,
360-
byte[] passwordData,
361-
out IntPtr itemRef);
362-
363-
[DllImport("/System/Library/Frameworks/Security.framework/Security")]
364-
private static extern int SecKeychainFindGenericPassword(
365-
IntPtr keychainOrArray,
366-
uint serviceNameLength,
367-
byte[] serviceName,
368-
uint accountNameLength,
369-
byte[] accountName,
370-
out uint passwordLength,
371-
out IntPtr passwordData,
372-
out IntPtr itemRef);
373-
374-
[DllImport("/System/Library/Frameworks/Security.framework/Security")]
375-
private static extern int SecKeychainItemModifyAttributesAndData(
376-
IntPtr itemRef,
377-
IntPtr attrList,
378-
uint length,
379-
byte[] data);
380-
381-
[DllImport("/System/Library/Frameworks/Security.framework/Security")]
382-
private static extern int SecKeychainItemDelete(IntPtr itemRef);
383-
384-
[DllImport("/System/Library/Frameworks/Security.framework/Security")]
385-
private static extern int SecKeychainItemFreeContent(IntPtr attrList, IntPtr data);
386-
387-
[DllImport("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")]
352+
private const int errSecSuccess = 0;
353+
private const int errSecItemNotFound = -25300;
354+
private const int errSecDuplicateItem = -25299;
355+
356+
private const string libCoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
357+
private const string libSecurity = "/System/Library/Frameworks/Security.framework/Security";
358+
359+
[DllImport(libCoreFoundation)]
360+
private static extern IntPtr CFStringCreateWithCString(IntPtr alloc, byte[] cStr, uint encoding);
361+
[DllImport(libCoreFoundation)]
362+
private static extern IntPtr CFDataCreate(IntPtr allocator, byte[] bytes, nint length);
363+
[DllImport(libCoreFoundation)]
364+
private static extern nint CFDataGetLength(IntPtr theData);
365+
[DllImport(libCoreFoundation)]
366+
private static extern IntPtr CFDataGetBytePtr(IntPtr theData);
367+
[DllImport(libCoreFoundation)]
368+
private static extern IntPtr CFDictionaryCreateMutable(IntPtr allocator, IntPtr capacity, IntPtr keyCallBacks, IntPtr valueCallBacks);
369+
[DllImport(libCoreFoundation)]
370+
private static extern void CFDictionarySetValue(IntPtr theDict, IntPtr key, IntPtr value);
371+
[DllImport(libCoreFoundation)]
388372
private static extern void CFRelease(IntPtr cf);
389373

374+
[DllImport(libSecurity)]
375+
private static extern int SecItemAdd(IntPtr attributes, IntPtr result);
376+
[DllImport(libSecurity)]
377+
private static extern int SecItemCopyMatching(IntPtr query, out IntPtr result);
378+
[DllImport(libSecurity)]
379+
private static extern int SecItemUpdate(IntPtr query, IntPtr attributesToUpdate);
380+
[DllImport(libSecurity)]
381+
private static extern int SecItemDelete(IntPtr query);
382+
383+
private static readonly Lazy<IntPtr> moduleCoreFoundation = new(() => NativeLibrary.Load(libCoreFoundation));
384+
private static readonly Lazy<IntPtr> moduleSecurity = new(() => NativeLibrary.Load(libSecurity));
385+
386+
private static IntPtr LoadSymbol(IntPtr module, string symbol) => Marshal.ReadIntPtr(NativeLibrary.GetExport(module, symbol));
387+
388+
private static readonly Lazy<IntPtr> kSecClass = new(() => LoadSymbol(moduleSecurity.Value, "kSecClass"));
389+
private static readonly Lazy<IntPtr> kSecClassGenericPassword = new(() => LoadSymbol(moduleSecurity.Value, "kSecClassGenericPassword"));
390+
private static readonly Lazy<IntPtr> kSecAttrService = new(() => LoadSymbol(moduleSecurity.Value, "kSecAttrService"));
391+
private static readonly Lazy<IntPtr> kSecAttrAccount = new(() => LoadSymbol(moduleSecurity.Value, "kSecAttrAccount"));
392+
private static readonly Lazy<IntPtr> kSecValueData = new(() => LoadSymbol(moduleSecurity.Value, "kSecValueData"));
393+
private static readonly Lazy<IntPtr> kSecReturnData = new(() => LoadSymbol(moduleSecurity.Value, "kSecReturnData"));
394+
private static readonly Lazy<IntPtr> kSecMatchLimit = new(() => LoadSymbol(moduleSecurity.Value, "kSecMatchLimit"));
395+
private static readonly Lazy<IntPtr> kSecMatchLimitOne = new(() => LoadSymbol(moduleSecurity.Value, "kSecMatchLimitOne"));
396+
private static readonly Lazy<IntPtr> kCFBooleanTrue = new(() => LoadSymbol(moduleCoreFoundation.Value, "kCFBooleanTrue"));
397+
390398
public void Store(string host, string username, string password)
391399
{
392-
var serviceBytes = Encoding.UTF8.GetBytes(host);
393-
var accountBytes = Encoding.UTF8.GetBytes(username);
394-
var secretBytes = Encoding.UTF8.GetBytes(password);
395-
396-
var status = SecKeychainAddGenericPassword(
397-
IntPtr.Zero,
398-
(uint)serviceBytes.Length, serviceBytes,
399-
(uint)accountBytes.Length, accountBytes,
400-
(uint)secretBytes.Length, secretBytes,
401-
out _);
402-
403-
if (status == errSecDuplicateItem)
400+
IntPtr query = IntPtr.Zero;
401+
IntPtr serviceCf = IntPtr.Zero;
402+
IntPtr accountCf = IntPtr.Zero;
403+
IntPtr dataCf = IntPtr.Zero;
404+
IntPtr attrsToUpdate = IntPtr.Zero;
405+
406+
try
404407
{
405-
// If the item already exists, update it instead
406-
status = SecKeychainFindGenericPassword(
407-
IntPtr.Zero,
408-
(uint)serviceBytes.Length, serviceBytes,
409-
(uint)accountBytes.Length, accountBytes,
410-
out _, out _, out var itemRef);
408+
serviceCf = CreateCFString(host);
409+
accountCf = CreateCFString(username);
410+
dataCf = CreateCFData(Encoding.UTF8.GetBytes(password));
411+
412+
query = CFDictionaryCreateMutable(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
413+
CFDictionarySetValue(query, kSecClass.Value, kSecClassGenericPassword.Value);
414+
CFDictionarySetValue(query, kSecAttrService.Value, serviceCf);
415+
CFDictionarySetValue(query, kSecAttrAccount.Value, accountCf);
416+
CFDictionarySetValue(query, kSecValueData.Value, dataCf);
417+
418+
int status = SecItemAdd(query, IntPtr.Zero);
419+
if (status == errSecSuccess)
420+
return;
411421

412-
if (status != errSecSuccess)
413-
throw new Exception($"Failed to find original keychain item to modify: {status}");
422+
if (status != errSecDuplicateItem)
423+
throw new Exception($"Failed to store credential to keychain: {status}");
414424

415-
status = SecKeychainItemModifyAttributesAndData(
416-
itemRef,
417-
IntPtr.Zero,
418-
(uint)secretBytes.Length, secretBytes);
425+
attrsToUpdate = CFDictionaryCreateMutable(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
426+
CFDictionarySetValue(attrsToUpdate, kSecValueData.Value, dataCf);
427+
status = SecItemUpdate(query, attrsToUpdate);
419428

420-
CFRelease(itemRef);
429+
if (status != errSecSuccess)
430+
throw new Exception($"Failed to update credential in keychain: {status}");
431+
}
432+
finally
433+
{
434+
if (query != IntPtr.Zero)
435+
CFRelease(query);
436+
if (serviceCf != IntPtr.Zero)
437+
CFRelease(serviceCf);
438+
if (accountCf != IntPtr.Zero)
439+
CFRelease(accountCf);
440+
if (dataCf != IntPtr.Zero)
441+
CFRelease(dataCf);
442+
if (attrsToUpdate != IntPtr.Zero)
443+
CFRelease(attrsToUpdate);
421444
}
422-
423-
if (status != errSecSuccess)
424-
throw new Exception($"Failed to store credential to keychain: {status}");
425445
}
426446

427447
public string Read(string host, string username)
428448
{
429-
var serviceBytes = Encoding.UTF8.GetBytes(host);
430-
var accountBytes = Encoding.UTF8.GetBytes(username);
449+
IntPtr query = IntPtr.Zero;
450+
IntPtr serviceCf = IntPtr.Zero;
451+
IntPtr accountCf = IntPtr.Zero;
452+
IntPtr dataCf = IntPtr.Zero;
431453

432-
var status = SecKeychainFindGenericPassword(
433-
IntPtr.Zero,
434-
(uint)serviceBytes.Length, serviceBytes,
435-
(uint)accountBytes.Length, accountBytes,
436-
out var passwordLength,
437-
out var passwordData,
438-
out var itemRef);
439-
440-
if (status == errSecItemNotFound)
441-
throw new Exception("Credential not found in keychain");
454+
try
455+
{
456+
serviceCf = CreateCFString(host);
457+
accountCf = CreateCFString(username);
458+
459+
query = CFDictionaryCreateMutable(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
460+
CFDictionarySetValue(query, kSecClass.Value, kSecClassGenericPassword.Value);
461+
CFDictionarySetValue(query, kSecAttrService.Value, serviceCf);
462+
CFDictionarySetValue(query, kSecAttrAccount.Value, accountCf);
463+
CFDictionarySetValue(query, kSecReturnData.Value, kCFBooleanTrue.Value);
464+
CFDictionarySetValue(query, kSecMatchLimit.Value, kSecMatchLimitOne.Value);
465+
466+
int status = SecItemCopyMatching(query, out dataCf);
467+
if (status == errSecItemNotFound)
468+
throw new Exception("Credential not found in keychain");
469+
if (status != errSecSuccess)
470+
throw new Exception($"Failed to read credential from keychain: {status}");
442471

443-
if (status != errSecSuccess)
444-
throw new Exception($"Failed to read credential from keychain: {status}");
472+
var len = CFDataGetLength(dataCf);
473+
if (len <= 0)
474+
return string.Empty;
445475

446-
if (passwordLength == 0 || passwordData == IntPtr.Zero)
476+
var ptr = CFDataGetBytePtr(dataCf);
477+
var bytes = new byte[len];
478+
Marshal.Copy(ptr, bytes, 0, (int)len);
479+
return Encoding.UTF8.GetString(bytes);
480+
}
481+
finally
447482
{
448-
CFRelease(itemRef);
449-
return string.Empty;
483+
if (query != IntPtr.Zero)
484+
CFRelease(query);
485+
if (serviceCf != IntPtr.Zero)
486+
CFRelease(serviceCf);
487+
if (accountCf != IntPtr.Zero)
488+
CFRelease(accountCf);
489+
if (dataCf != IntPtr.Zero)
490+
CFRelease(dataCf);
450491
}
451-
452-
var passwordBytes = new byte[passwordLength];
453-
Marshal.Copy(passwordData, passwordBytes, 0, (int)passwordLength);
454-
CFRelease(itemRef);
455-
return Encoding.UTF8.GetString(passwordBytes);
456492
}
457493

458494
public void Erase(string host, string username)
459495
{
460-
var serviceBytes = Encoding.UTF8.GetBytes(host);
461-
var accountBytes = Encoding.UTF8.GetBytes(username);
462-
463-
var status = SecKeychainFindGenericPassword(
464-
IntPtr.Zero,
465-
(uint)serviceBytes.Length, serviceBytes,
466-
(uint)accountBytes.Length, accountBytes,
467-
out var passwordLength,
468-
out var passwordData,
469-
out var itemRef);
470-
471-
if (status == errSecItemNotFound)
472-
return;
496+
IntPtr query = IntPtr.Zero;
497+
IntPtr serviceCf = IntPtr.Zero;
498+
IntPtr accountCf = IntPtr.Zero;
499+
500+
try
501+
{
502+
serviceCf = CreateCFString(host);
503+
accountCf = CreateCFString(username);
473504

474-
if (status != errSecSuccess)
475-
throw new Exception($"Failed to find credential in keychain: {status}");
505+
query = CFDictionaryCreateMutable(IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
506+
CFDictionarySetValue(query, kSecClass.Value, kSecClassGenericPassword.Value);
507+
CFDictionarySetValue(query, kSecAttrService.Value, serviceCf);
508+
CFDictionarySetValue(query, kSecAttrAccount.Value, accountCf);
476509

477-
status = SecKeychainItemDelete(itemRef);
478-
CFRelease(itemRef);
510+
int status = SecItemDelete(query);
511+
if (status == errSecSuccess || status == errSecItemNotFound)
512+
return;
479513

480-
if (status != errSecSuccess && status != errSecItemNotFound)
481514
throw new Exception($"Failed to delete credential from keychain: {status}");
515+
}
516+
finally
517+
{
518+
if (query != IntPtr.Zero)
519+
CFRelease(query);
520+
if (serviceCf != IntPtr.Zero)
521+
CFRelease(serviceCf);
522+
if (accountCf != IntPtr.Zero)
523+
CFRelease(accountCf);
524+
}
482525
}
483526

484-
private const int errSecSuccess = 0;
485-
private const int errSecItemNotFound = -25300;
486-
private const int errSecDuplicateItem = -25299;
527+
private IntPtr CreateCFString(string str)
528+
{
529+
var bytes = Encoding.UTF8.GetBytes(str + "\0");
530+
var ptr = CFStringCreateWithCString(IntPtr.Zero, bytes, 0x08000100 /* kCFStringEncodingUTF8 */);
531+
if (ptr == IntPtr.Zero)
532+
throw new InvalidOperationException("CFStringCreateWithCString failed.");
533+
return ptr;
534+
}
535+
536+
private IntPtr CreateCFData(byte[] bytes)
537+
{
538+
var ptr = CFDataCreate(IntPtr.Zero, bytes, bytes.Length);
539+
if (ptr == IntPtr.Zero)
540+
throw new InvalidOperationException("CFDataCreate failed.");
541+
return ptr;
542+
}
487543
}
488544
}

0 commit comments

Comments
 (0)