@@ -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