WaxSealCoreOS X Keychain 的面向?qū)ο蠓庋b
什么是 WaxSealCore
WaxSealCore 是一個受 Cocoa 設(shè)計(jì)影響的代碼庫,由 @開源中國真理部部長 用 Objective-C 編寫。其對 OS X Keychain Services API 進(jìn)行了面向?qū)ο蠓庋b,使得 Mac 開發(fā)者更容易地將 Keychain 機(jī)制融入到自己的 app 中。相對于 Apple 官方的 Keychain Services API 來說:
-
完全面向?qū)ο?
-
API 風(fēng)格和 Cocoa 非常接近,熟練的 Mac 開發(fā)者可以迅速上手
-
支持基于 Unicode 字符搜索密碼項(xiàng)
-
詳盡的文檔支持
什么是 Keychain Services
OS X 和 iOS 開發(fā)者對 “鑰匙串 API”(Keychain Services API,為消歧義,下文都使用英文名稱)應(yīng)該都有所耳聞,計(jì)算機(jī)用戶總是必須管理許多用戶 ID 和密碼,比如在瀏覽器中的 Twitter,F(xiàn)acebook,OSChina.net 等網(wǎng)站,以及 Evernote,Skype,Telegram 等桌面和移動 app 的登錄密碼。這些服務(wù)在你能夠使用之前都需要通過密碼來驗(yàn)證使用者的身份。因?yàn)槊艽a繁雜,所以很多用戶總是通過起一個非常簡單非常容易記住的密碼,并且為多個服務(wù)使用相同的密碼來應(yīng)付這件事(甚至將密碼寫在隨手能夠找到的小紙條上的也大有人在)。這些做法都大大削弱了密碼的安全性。
所以在 OS X 和 iOS 中有一個被稱為 Keychain 的機(jī)制(平時你可以通過 OS X 自帶的 Keychain Access 應(yīng)用訪問系統(tǒng)中的 Keychain),Keychain 是一種具有特殊格式的文件類型(.keychain 文件),其是一個安全的加密容器,其本身可以使用一個主密碼(master password)進(jìn)行鎖定,除了密碼的擁有人,沒有人能夠訪問這個加密容器中的任何內(nèi)容。OS X 和 iOS 用戶在訪問一個新的網(wǎng)站時,就會被詢問是否要保存網(wǎng)站的密碼,以便下次自動登錄,當(dāng)用戶點(diǎn)擊“保存”時,用戶的密碼就是被保存到這個加密容器內(nèi),Keychain 會對你輸入的密碼進(jìn)行高強(qiáng)度的加密,然后存儲在其中,下次訪問時通過解密密碼既可以實(shí)現(xiàn)自動登錄。
Keychain Services 是 Keychain 機(jī)制的編程接口。OS X/iOS 開發(fā)者在開發(fā)應(yīng)用時,只需要調(diào)用這套中的函數(shù),就可以將自己的應(yīng)用中用到的密碼存儲到 OS X/iOS 的 Keychain 中,下次需要使用密碼時可以直接從 Keychain 中進(jìn)行獲取而不必每次都讓用戶重新輸入。除此之外,對于 Mac 開發(fā)者來說,你的應(yīng)用還可以和其他應(yīng)用共享同一個服務(wù)的密碼。Keychain Services 是一個很方便的 API,它無需開發(fā)者自己實(shí)現(xiàn)一套密碼管理機(jī)制。
事實(shí)上,OS X 版的 Firefox 和 Thunderbird 就有一個廣為詬病的問題,就是它們都使用自己實(shí)現(xiàn)的密碼管理器而不使用 Keychain,這有兩個弊端:
OS X 用戶習(xí)慣使用 Keychain 并建立了信任。如果提供自己的密碼管理器,那么用戶對它的信任度跟對你的信任度是一樣的,一般來說不如他們對 Apple 公司的信任度。
用戶不能在你的應(yīng)用程序之外訪問密碼。例如,Mac 版的 Chrome,Safari 和 Opera 就都能夠共享 Web 的登錄資料,因?yàn)樗鼈兌际褂?Keychain,并且用戶可以用 Keychain Access 應(yīng)用來修改他們看到的密碼。
-- David Chisnall, Cocoa Programming Developer's Handbook
上面只是簡單介紹了一下 Keychain 機(jī)制和它的 API,它們的功能遠(yuǎn)不止存取密碼這么簡單,只不過這些功能是最常用到的。Keychain Services 這套 API 很強(qiáng)大,但是缺點(diǎn)就是,它的接口是純 C 的,丑陋,復(fù)雜,并且因?yàn)樗腔?Core Foundation 的,所以需要你手動管理內(nèi)存(不像 Cocoa/Cocoa-Touch 可以利用引用計(jì)數(shù)和自動釋放池),所以極易產(chǎn)生 bug。再加上 Keychain Services 的文檔很古老,有很多錯誤都會無故地增大學(xué)習(xí)曲線,所以,最終,我實(shí)在受夠它了,懶惰是程序員得美德,于是我找了一些開源的 Objective-C wrapper,這些 wrapper 雖然簡化了使用,但是功能上要么太簡陋(只能存取 generic password 和 Internet password,而沒有實(shí)現(xiàn) Access Control List 這類強(qiáng)大的功能),要么年代久遠(yuǎn)。所以決定自己寫一個全特性的封裝,而不僅是限于存取密碼這種簡單的功能。
Keychain Services vs. WaxSealCore
@紅薯 說得好,框架的作者們不要總吹噓自己的框架多么好用,而是要看你的框架能夠?qū)崒?shí)在在地為開發(fā)者節(jié)省多少代碼,所以用兩個功能來比較一下 WaxSealCore 和純 C 的 Keychain Services。
-
使用一個顯示指定的密碼常見一個空的 Keychain
使用 Keychain Services 的純 C 接口實(shí)現(xiàn):
OSStatus resultCode = errSecSuccess; SecKeychainRef secEmptyKeychain = NULL; NSURL* URL = [ [ [ NSBundle mainBundle ] bundleURL ] URLByAppendingPathComponent: @"EmptyKeychainForWiki.keychain" ]; char* passphrase = "waxsealcore"; // Create an empty keychain with given passphrase resultCode = SecKeychainCreate( URL.path.UTF8String , ( UInt32 )strlen( passphrase ) , ( void const* )passphrase , ( Boolean )NO , NULL , &secEmptyKeychain ); NSAssert( resultCode == errSecSuccess, @"Failed to create new empty keychain" ); resultCode = SecKeychainDelete( secEmptyKeychain ); NSAssert( resultCode == errSecSuccess, @"Failed to delete the given keychain" ); if ( secEmptyKeychain ) // Keychain Services is based on Core Foundation, // you have to manage the memory manually CFRelease( secEmptyKeychain );
使用 WaxSealCore 實(shí)現(xiàn):
NSError* error = nil; // Create an empty keychain with given passphrase WSCKeychain* emptyKeychain = [ [ WSCKeychainManager defaultManager ] createKeychainWithURL: [ [ [ NSBundle mainBundle ] bundleURL ] URLByAppendingPathComponent: @"EmptyKeychainForWiki.keychain" ] passphrase: @"waxsealcore" becomesDefault: NO error: &error ]; // You have no need for managing the memory manually, // emptyKeychain will be released automatically.
-
查找下面截圖中的這個密碼項(xiàng),并且獲取它的賬戶名,密碼和注釋信息(注釋信息含有中文,Keychain Services 無法進(jìn)行查找)
使用 Keychain Services 的純 C 接口實(shí)現(xiàn):
OSStatus resultCode = errSecSuccess;
// Attributes that will be used for constructing search criteria
char* label = "secure.imdb.com";
SecProtocolType* ptrProtocolType = malloc( sizeof( SecProtocolType ) );
*ptrProtocolType = kSecProtocolTypeHTTPS;
SecKeychainAttribute attrs[] = { { kSecLabelItemAttr, ( UInt32 )strlen( label ), ( void* )label }
, { kSecProtocolItemAttr, ( UInt32 )sizeof( SecProtocolType ), ( void* )ptrProtocolType }
};
SecKeychainAttributeList attrsList = { sizeof( attrs ) / sizeof( attrs[ 0 ] ), attrs };
// Creates a search object matching the given list of search criteria.
SecKeychainSearchRef searchObject = NULL;
if ( ( resultCode = SecKeychainSearchCreateFromAttributes( NULL
, kSecInternetPasswordItemClass
, &attrsList
, &searchObject
) ) == errSecSuccess )
{
SecKeychainItemRef matchedItem = NULL;
// Finds the next keychain item matching the given search criteria.
while ( ( resultCode = SecKeychainSearchCopyNext( searchObject, &matchedItem ) ) != errSecItemNotFound )
{
SecKeychainAttribute theAttributes[] = { { kSecAccountItemAttr, 0, NULL }
, { kSecCommentItemAttr, 0, NULL }
};
SecKeychainAttributeList theAttrList = { sizeof( theAttributes ) / sizeof( theAttributes[ 0 ] ), theAttributes };
UInt32 lengthOfPassphrase = 0;
char* passphraseBuffer = NULL;
if ( ( resultCode = SecKeychainItemCopyContent( matchedItem
, NULL
, &theAttrList
, &lengthOfPassphrase
, ( void** )&passphraseBuffer
) ) == errSecSuccess )
{
NSLog( @"\n==============================\n" );
NSLog( @"Passphrase: %@", [ [ [ NSString alloc ] initWithBytes: passphraseBuffer length: lengthOfPassphrase encoding: NSUTF8StringEncoding ] autorelease ] );
for ( int _Index = 0; _Index < theAttrList.count; _Index++ )
{
SecKeychainAttribute attrStruct = theAttrList.attr[ _Index ];
NSString* attributeValue = [ [ [ NSString alloc ] initWithBytes: attrStruct.data length: attrStruct.length encoding: NSUTF8StringEncoding ] autorelease ];
if ( attrStruct.tag == kSecAccountItemAttr )
NSLog( @"IMDb User Name: %@", attributeValue );
else if ( attrStruct.tag == kSecCommentItemAttr )
NSLog( @"Comment: %@", attributeValue );
}
NSLog( @"\n==============================\n" );
}
SecKeychainItemFreeContent( &theAttrList, passphraseBuffer );
CFRelease( matchedItem );
}
}
if ( ptrProtocolType )
free( ptrProtocolType );
if ( searchObject )
CFRelease( searchObject );
使用 WaxSealCore 實(shí)現(xiàn):
只需一個方法的調(diào)用即可實(shí)現(xiàn):
NSError* error = nil;
WSCPassphraseItem* IMDbLoginPassphrase = ( WSCPassphraseItem* )[ [ WSCKeychain login ]
findFirstKeychainItemSatisfyingSearchCriteria: @{ WSCKeychainItemAttributeLabel : @"secure.imdb.com"
, WSCKeychainItemAttributeProtocol : WSCInternetProtocolCocoaValue( WSCInternetProtocolTypeHTTPS )
, WSCKeychainItemAttributeComment : @"這是一個用于演示 WaxSealCore 的密碼項(xiàng)"
}
itemClass: WSCKeychainItemClassInternetPassphraseItem
error: &error ];
// WaxSealCore supports Unicode-based search, so you can use Emoji or Chinese in your search criteria.
// One step. So easy, is not it?
打印賬戶名,密碼,和注釋,并且更改注釋內(nèi)容:
if ( IMDbLoginPassphrase )
{
NSLog( @"==============================" );
// Use the `account` property
NSLog( @"IMDb User Name: %@", IMDbLoginPassphrase.account );
// Use the `passphrase` property
NSLog( @"Passphrase: %@", [ [ [ NSString alloc ] initWithData: IMDbLoginPassphrase.passphrase encoding: NSUTF8StringEncoding ] autorelease ] );
// Use the `comment` property
NSLog( @"Comment: %@", IMDbLoginPassphrase.comment );
NSLog( @"==============================" );
// -setComment:
IMDbLoginPassphrase.comment = @"IMDb Passphrase";
}
else
NSLog( @"I'm so sorry!" );
簡單地進(jìn)行批量搜索:
// Find all the Internet passphrases that met the given search criteria
NSArray* passphrases = [ [ WSCKeychain login ]
// Batch search
findAllKeychainItemsSatisfyingSearchCriteria: @{ WSCKeychainItemAttributeLabel : @"secure.imdb.com"
, WSCKeychainItemAttributeProtocol : WSCInternetProtocolCocoaValue( WSCInternetProtocolTypeHTTPS )
, WSCKeychainItemAttributeComment : @"IMDb Passphrase"
}
itemClass: WSCKeychainItemClassInternetPassphraseItem
error: &error ];
if ( passphrases.count != 0 )
{
for ( WSCPassphraseItem* _Passphrase in passphrases )
{
NSLog( @"==============================" );
NSLog( @"IMDb User Name: %@", IMDbLoginPassphrase.account );
NSLog( @"Passphrase: %@", [ [ [ NSString alloc ] initWithData: IMDbLoginPassphrase.passphrase encoding: NSUTF8StringEncoding ] autorelease ] );
NSLog( @"Comment: %@", IMDbLoginPassphrase.comment );
NSLog( @"==============================" );
_Passphrase.comment = @"這是一個用于演示 WaxSealCore 的密碼項(xiàng)";
}
}
else
NSLog( @"I'm so sorry!" );
上面的演示可以看到,使用 Keychain Services 費(fèi)很大力氣需要完成的工作,用 WaxSealCore 寥寥幾行代碼即可做到。除此之外,WaxSealCore 還簡化了 Keychain 中的 Access Control List 機(jī)制,你可以更容易地使用 Keychain 更強(qiáng)大的功能。更多 API 的使用方式,可以參考我正在維護(hù)的一個 Wiki,歡迎任何人來編輯這個 wiki 頁面。
WaxSealCore 是自由軟件,在 MIT 許可證下發(fā)布,你可以在這里獲取源碼,自由修改或重新分發(fā)源代碼。如果不想自己編譯代碼,可以在這里獲取到我用我的開發(fā)者證書簽名的二進(jìn)制框架包。
Next Step
Keychain Services 不僅僅能夠存取普通密碼,同時還能夠存取數(shù)字證書(digital certificates),私鑰(private keys)等私密數(shù)據(jù),WaxSealCore 下一個版本就要提供對跟 Keychain Services 同處于 Security.framework 框架中的 Certificate, Key, and Trust Services API 的封裝,將融合對數(shù)字證書,對稱密鑰和非對稱密鑰的存取與操作,敬請期待。
獲取 WaxSealCore
聯(lián)系作者
如果你有任何問題,可以給我發(fā)郵件 dG9yaW5Aa3dvay5pbQ==(base64ed)。
