例えばtwitterに投稿できるアプリ作ろうと思うと…
使う側としてはパスワードを毎回要求されるより、記憶して自動で実行してくれた方がありがたい。けど、だからって、うかつなところに素のテキストで保存されたりすると、他人にパスワードさらされちゃうわけですよ。
そのためアプリ制作者はパスワードを暗号化してからファイルに書きだしたりするわけですが、暗号化は結構めんどくさい。
で、それをAppleが面倒みちゃおうじゃな~いと用意してくれたのが
非常にありがたい。
こいつは不特定多数のパスワードを暗号化して管理してくれる機構。
このキーチェーン、わしら結構、利用してます。ネットワークで別マシンのボリュームマウントしようとすると、下のようなダイアログ出てくるでしょ。
というかパスワードに限らず、証明書や秘密鍵なんかも管理できるようになってるんだけど(XcodeでのアプリのiPhone実機インストール時に、開発者証明書を取り出すためにXcodeがキーチェインにアクセスする事を許すか問い合わされたの覚えてる?)、今回はパスワードについて調べていきます。
証明書や認証局、秘密鍵なんかに興味のある人は
Certificate, Key, and Trust Services Programming Guide
あたりを読みましょう。
私が今回読み込んだのは
Keychain Services Programming Guide
の方です。
で、今回話題にしてるどっかのサーバーにログインするために使うアカウントとパスワードてのは最低限の構成として
で構成されるわけです。
例えばこの花は?にログインするためのアカウント名がxccでパスワードがtestなら
なんてふうに記憶し、これを検索して取り出して使うって事になるわけですな。
使うKeychain APIは
の2つのAPIでフローとしてはこんな感じ。
引数も
と一見、単純そうなんだけど…
受け渡しにNSDictionary使う(CFDictionaryRefってのはC言語用に用意されたNSDictionary*と等価の型)ってのがクセモンなんだよね~。
辞書なんで組み合わせが無限になっちゃうわけですよ。
例えばさっきの、この花は?にログインするためのアカウント名で検索するならSecItemCopyMatchingに渡すNSDictionaryは
kSecMatchLimit:検索で見つかった項目をすべて返すか、最初に見つかった1項目だけ返すかなど。
kSecReturnAttributes:属性を返すか返さないか。
という検索方法の指定と
kSecClass:保存項目の分類= kSecClassGenericPassword
kSecAttrService:サービス名=konohana
kSecAttrAccount:アカウント名=xcc
という検索対象の指定といったものになるわけっす。
で第2引き数のresultに結果が返される、っと。
結果はkSecMatchLimitになにを指定するかで変化して見つかった最初の1個を返すkSecMatchLimitOne、条件にあてはまるすべての項目を返すkSecMatchLimitAllではそれぞれ
になります。
で、もしこれで何も見つからなかったら、SecItemAddで新規登録となるわけで、こっちに渡すNSDictionaryは
kSecClass:保存項目の分類= kSecClassGenericPassword
kSecAttrService:サービス名=konohana
kSecAttrAccount:アカウント名=xcc
kSecValueData:パスワード
となるわけですな。
ここで指定しているkSecClassGenericPasswordでは、kSecAttrServiceとkSecAttrAccountの組み合わせで登録項目を作れるみたいです。
ということで、そろそろ実践してみます。
Xcodeを立ち上げ、ファイル>新規プロジェクトでiOS ApplicationのNavigation-based Applicationを選択。名前はkeychainとします。Core Dataはオフね。
今回はkeychainAppDelegate.mをいじってコンソールで実験するだけなんで、Navigation-based Applicationにする必要は無いんだけど、そのまま「この花は?」のログイン画面のテスターにするつもりなんでNavigation-based選んでます。
まずは
フレームワークの組み込み。
フレームワークの組み込みはドリル「フレームワークの追加方法」を参考にしてね。
で、それが終わったらkeychainAppDelegate.mに
とヘッダーをインポートしてapplication:didFinishLaunchingWithOptions:メソッドにテストメソッドの呼び出しを書きこむ。
これであとはkeychainTestメソッドでいろいろ試すわけだけど、とりあえずフローの最初にやるSecItemCopyMatchingを使った検索から。
辞書NSMutableDictionaryを作って,検索対象に
を指定して検索させてます。
でもって検索側のseek:では受け取ったNSMutableDictionaryに検索方法
を設定してSecItemCopyMatchingを呼び出し、返されたNSArrayを順にコンソールに表示っす。
ところどころ引数に渡す時に(id)とキャスト(型変換)宣言してる理由は、例えばkSecMatchLimitAllなんかがCFTypeRef型に宣言されてて、そのままだとワーニングが出るから。SecItemCopyMatchingにqueryDicを渡す時に(CFDictionaryRef)としてるのも同じ理由です。
C言語で使えるように、CFTypeRef型やCFDictionaryRef型ってのを用意してるけど、CFTypeRef型はNSStringオブジェクト、CFDictionaryRef型はNSDictionaryオブジェクトを渡せるわけですな。
とすることで、NSDictionaryの内容が表示できます。ただしこのままだとNSStringオブジェクトなんでUTF8String呼び出しでprintfで使えるC文字列を取り出してるわけです。
用意ができたんで、実行じゃ~ん。
っていってもこのままシミュレータ起動しても何もわかりません。コンソール出しましょう。
コンソールの出し方がわからん人はドリル「コンソールを表示する」を読むように。
という残念な結果。
エラー値の説明はSecBase.hに書かれてます。
こいつね↑
SecItemCopyMatchingを選択してコンテキストメニュー(ドリル「コンソールを表示する」)から定義へジャンプメニュー選択、SecItemCopyMatchingの説明部分にある
のSecBase.hを選択しておいてXcodeのメニューのファイル>すばやく開く…メニューでも開けるよ。
説明されてるエラーは
あたりまえ。
今度は事前にKeyChainにkSecAttrServiceがkonohanaの項目を登録しておいてから検索してみます。
の必要最小限で登録。
実行するとこんな感じ。
SecItemAddは無事成功(0が成功ね)し、検索もできとります。自分が指定したのはkSecClassとkSecAttrServiceなんだけど、他に2項目追加されとりますな。たぶん、これが必要最小限な情報。
無事成功したので、アプリ終了して再度実行してみると
SecItemAdd error = -25299
となるんですな。
追加しようとした項目はすでに存在するので無理ー。
というわけです。
これまたあたりまえ。なので検索にはちゃんとヒットしてます。
面白いのは、アプリをシミュレータのスプリングボード(アプリアイコンが並んでる画面ね)から削除して、再度実行しても
SecItemAdd error = -25299
となること。
普通、NSUserDefaults(その(44)覚えてるかな~)でもスプリングボードから削除しちゃえば前の情報は残らないわけですが、KeyChainはシステム側が初期化でもされない限り残るんですな。
とりあえず追加された2項目の属性はなにかなーとkSecClassGenericPasswordクラスの全要素を出力してみたんすよ。
結果がこれ。
おお、kSecClassは無くなってるわけね。なので追加されてるのは3つ。
アクセス権源は、そのKeyChain項目にアクセスするのに承認を必要とさせるかとかの設定みたいっす。"ak"とあるけど同じ方法で調べたらkSecAttrAccessibleWhenUnlockedでした。
アクセスグループてのは、複数のアプリケーションで特定のKeyChain項目を共有するための設定で、正しく設定すると別々のiPhoneアプリからアクセスできるようになる。なにも指定しないとtestグループに配属されるみたい。
Appleが用意してくれているサンプルGenericKeychainで実装してくれているんだけど、イントロにちょこっと別アプリケーション間で共有する方法のサンプルとか書いてるんだけで、詳しい説明はGenericKeychain/ReadMe.txt側に書かれてるので、ぱっと見、なんじゃこりゃって思いますな。
ターゲットがGenericKeychain、GenericKeychain2と2つある事自体に気づいてない人多いんじゃねーか?
Appleのサンプルソース:GenericKeychain
アカウントは説明済みですな。
てなところで、今回はこれまで。
------------
サンプルプロジェクト:keychain.zip
使う側としてはパスワードを毎回要求されるより、記憶して自動で実行してくれた方がありがたい。けど、だからって、うかつなところに素のテキストで保存されたりすると、他人にパスワードさらされちゃうわけですよ。
そのためアプリ制作者はパスワードを暗号化してからファイルに書きだしたりするわけですが、暗号化は結構めんどくさい。
で、それをAppleが面倒みちゃおうじゃな~いと用意してくれたのが
Keychain Services
非常にありがたい。
こいつは不特定多数のパスワードを暗号化して管理してくれる機構。
このキーチェーン、わしら結構、利用してます。ネットワークで別マシンのボリュームマウントしようとすると、下のようなダイアログ出てくるでしょ。
というかパスワードに限らず、証明書や秘密鍵なんかも管理できるようになってるんだけど(XcodeでのアプリのiPhone実機インストール時に、開発者証明書を取り出すためにXcodeがキーチェインにアクセスする事を許すか問い合わされたの覚えてる?)、今回はパスワードについて調べていきます。
証明書や認証局、秘密鍵なんかに興味のある人は
Certificate, Key, and Trust Services Programming Guide
あたりを読みましょう。
私が今回読み込んだのは
Keychain Services Programming Guide
の方です。
で、今回話題にしてるどっかのサーバーにログインするために使うアカウントとパスワードてのは最低限の構成として
サービス名
アカウント名
パスワード
アカウント名
パスワード
で構成されるわけです。
例えばこの花は?にログインするためのアカウント名がxccでパスワードがtestなら
サービス名:konohana
アカウント名:xcc
パスワード:test
アカウント名:xcc
パスワード:test
なんてふうに記憶し、これを検索して取り出して使うって事になるわけですな。
使うKeychain APIは
検索:SecItemCopyMatching
追加:SecItemAdd
追加:SecItemAdd
の2つのAPIでフローとしてはこんな感じ。
引数も
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
と一見、単純そうなんだけど…
受け渡しにNSDictionary使う(CFDictionaryRefってのはC言語用に用意されたNSDictionary*と等価の型)ってのがクセモンなんだよね~。
辞書なんで組み合わせが無限になっちゃうわけですよ。
例えばさっきの、この花は?にログインするためのアカウント名で検索するならSecItemCopyMatchingに渡すNSDictionaryは
kSecMatchLimit:検索で見つかった項目をすべて返すか、最初に見つかった1項目だけ返すかなど。
kSecReturnAttributes:属性を返すか返さないか。
という検索方法の指定と
kSecClass:保存項目の分類= kSecClassGenericPassword
kSecAttrService:サービス名=konohana
kSecAttrAccount:アカウント名=xcc
という検索対象の指定といったものになるわけっす。
で第2引き数のresultに結果が返される、っと。
結果はkSecMatchLimitになにを指定するかで変化して見つかった最初の1個を返すkSecMatchLimitOne、条件にあてはまるすべての項目を返すkSecMatchLimitAllではそれぞれ
kSecMatchLimitOne:NSDictionary
kSecMatchLimitAll:NSDictionaryで構成されるNSArray
kSecMatchLimitAll:NSDictionaryで構成されるNSArray
になります。
で、もしこれで何も見つからなかったら、SecItemAddで新規登録となるわけで、こっちに渡すNSDictionaryは
kSecClass:保存項目の分類= kSecClassGenericPassword
kSecAttrService:サービス名=konohana
kSecAttrAccount:アカウント名=xcc
kSecValueData:パスワード
となるわけですな。
ここで指定しているkSecClassGenericPasswordでは、kSecAttrServiceとkSecAttrAccountの組み合わせで登録項目を作れるみたいです。
ということで、そろそろ実践してみます。
Xcodeを立ち上げ、ファイル>新規プロジェクトでiOS ApplicationのNavigation-based Applicationを選択。名前はkeychainとします。Core Dataはオフね。
今回はkeychainAppDelegate.mをいじってコンソールで実験するだけなんで、Navigation-based Applicationにする必要は無いんだけど、そのまま「この花は?」のログイン画面のテスターにするつもりなんでNavigation-based選んでます。
まずは
Security.framework
フレームワークの組み込み。
フレームワークの組み込みはドリル「フレームワークの追加方法」を参考にしてね。
で、それが終わったらkeychainAppDelegate.mに
#import <Security/Security.h>
とヘッダーをインポートしてapplication:didFinishLaunchingWithOptions:メソッドにテストメソッドの呼び出しを書きこむ。
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self keychainTest];
// Add the navigation controller's view to the window and display.
[self.window addSubview:navigationController.view];
[self.window makeKeyAndVisible];
return YES;
}
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self keychainTest];
// Add the navigation controller's view to the window and display.
[self.window addSubview:navigationController.view];
[self.window makeKeyAndVisible];
return YES;
}
これであとはkeychainTestメソッドでいろいろ試すわけだけど、とりあえずフローの最初にやるSecItemCopyMatchingを使った検索から。
-(void)seek:(NSMutableDictionary*)queryDic {
// 検索条件設定
// 一般的なパスワード
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
// 条件に合う項目すべてをリストアップ
[queryDic setObject:(id)kSecMatchLimitAll forKey:(id)kSecMatchLimit];
// リストする項目の属性情報を取り出す。
[queryDic setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
// 検索開始!
NSArray* array = nil;
OSStatus keychainErr = SecItemCopyMatching((CFDictionaryRef)queryDic,
(CFTypeRef *)&array);
// 結果発表
if (keychainErr == noErr) {
for (NSMutableDictionary *dic in array) {
printf("dic = %s\n", [[dic description] UTF8String]);
}
} else {
printf("SecItemCopyMatching error = %d\n", (int)keychainErr);
}
}
-(void)keychainTest {
// 問い合わせ用辞書
NSMutableDictionary* queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:@"konohana" forKey:(id)kSecAttrService];
[self seek:queryDic];
}
// 検索条件設定
// 一般的なパスワード
[queryDic setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
// 条件に合う項目すべてをリストアップ
[queryDic setObject:(id)kSecMatchLimitAll forKey:(id)kSecMatchLimit];
// リストする項目の属性情報を取り出す。
[queryDic setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
// 検索開始!
NSArray* array = nil;
OSStatus keychainErr = SecItemCopyMatching((CFDictionaryRef)queryDic,
(CFTypeRef *)&array);
// 結果発表
if (keychainErr == noErr) {
for (NSMutableDictionary *dic in array) {
printf("dic = %s\n", [[dic description] UTF8String]);
}
} else {
printf("SecItemCopyMatching error = %d\n", (int)keychainErr);
}
}
-(void)keychainTest {
// 問い合わせ用辞書
NSMutableDictionary* queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:@"konohana" forKey:(id)kSecAttrService];
[self seek:queryDic];
}
辞書NSMutableDictionaryを作って,検索対象に
kSecAttrService(サービス名) = "konohana"
を指定して検索させてます。
でもって検索側のseek:では受け取ったNSMutableDictionaryに検索方法
kSecClass(探すクラス)= kSecClassGenericPassword
kSecMatchLimit(検索数の限界)= kSecMatchLimitAll
kSecReturnAttributes(返す内容) = kCFBooleanTrue
kSecMatchLimit(検索数の限界)= kSecMatchLimitAll
kSecReturnAttributes(返す内容) = kCFBooleanTrue
を設定してSecItemCopyMatchingを呼び出し、返されたNSArrayを順にコンソールに表示っす。
ところどころ引数に渡す時に(id)とキャスト(型変換)宣言してる理由は、例えばkSecMatchLimitAllなんかがCFTypeRef型に宣言されてて、そのままだとワーニングが出るから。SecItemCopyMatchingにqueryDicを渡す時に(CFDictionaryRef)としてるのも同じ理由です。
C言語で使えるように、CFTypeRef型やCFDictionaryRef型ってのを用意してるけど、CFTypeRef型はNSStringオブジェクト、CFDictionaryRef型はNSDictionaryオブジェクトを渡せるわけですな。
[dic description]
とすることで、NSDictionaryの内容が表示できます。ただしこのままだとNSStringオブジェクトなんでUTF8String呼び出しでprintfで使えるC文字列を取り出してるわけです。
用意ができたんで、実行じゃ~ん。
っていってもこのままシミュレータ起動しても何もわかりません。コンソール出しましょう。
コンソールの出し方がわからん人はドリル「コンソールを表示する」を読むように。
という残念な結果。
エラー値の説明はSecBase.hに書かれてます。
こいつね↑
SecItemCopyMatchingを選択してコンテキストメニュー(ドリル「コンソールを表示する」)から定義へジャンプメニュー選択、SecItemCopyMatchingの説明部分にある
@result A result code. See "Security Error Codes" (SecBase.h).
のSecBase.hを選択しておいてXcodeのメニューのファイル>すばやく開く…メニューでも開けるよ。
説明されてるエラーは
errSecItemNotFound
あたりまえ。
今度は事前にKeyChainにkSecAttrServiceがkonohanaの項目を登録しておいてから検索してみます。
kSecClass(登録クラス)= kSecClassGenericPassword
kSecAttrService(サービス名) = "konohana"
kSecAttrService(サービス名) = "konohana"
の必要最小限で登録。
-(void)keychainTest {
// 事前登録
NSMutableDictionary* item = [NSMutableDictionary dictionary];
[item setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[item setObject:@"konohana" forKey:(id)kSecAttrService];
OSStatus status = SecItemAdd((CFDictionaryRef)item, nil);
printf("SecItemAdd error = %d\n", (int)status);
// 検索
// 問い合わせ用辞書
NSMutableDictionary* queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:@"konohana" forKey:(id)kSecAttrService];
[self seek:queryDic];
}
// 事前登録
NSMutableDictionary* item = [NSMutableDictionary dictionary];
[item setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[item setObject:@"konohana" forKey:(id)kSecAttrService];
OSStatus status = SecItemAdd((CFDictionaryRef)item, nil);
printf("SecItemAdd error = %d\n", (int)status);
// 検索
// 問い合わせ用辞書
NSMutableDictionary* queryDic = [NSMutableDictionary dictionary];
[queryDic setObject:@"konohana" forKey:(id)kSecAttrService];
[self seek:queryDic];
}
実行するとこんな感じ。
SecItemAddは無事成功(0が成功ね)し、検索もできとります。自分が指定したのはkSecClassとkSecAttrServiceなんだけど、他に2項目追加されとりますな。たぶん、これが必要最小限な情報。
無事成功したので、アプリ終了して再度実行してみると
SecItemAdd error = -25299
となるんですな。
errSecDuplicateItem
追加しようとした項目はすでに存在するので無理ー。
というわけです。
これまたあたりまえ。なので検索にはちゃんとヒットしてます。
面白いのは、アプリをシミュレータのスプリングボード(アプリアイコンが並んでる画面ね)から削除して、再度実行しても
SecItemAdd error = -25299
となること。
普通、NSUserDefaults(その(44)覚えてるかな~)でもスプリングボードから削除しちゃえば前の情報は残らないわけですが、KeyChainはシステム側が初期化でもされない限り残るんですな。
とりあえず追加された2項目の属性はなにかなーとkSecClassGenericPasswordクラスの全要素を出力してみたんすよ。
printf("%s\n", [(id)kSecClass UTF8String]);
printf("%s\n", [(id)kSecAttrAccessible UTF8String]);
printf("%s\n", [(id)kSecAttrAccessGroup UTF8String]);
printf("%s\n", [(id)kSecAttrCreationDate UTF8String]);
printf("%s\n", [(id)kSecAttrModificationDate UTF8String]);
printf("%s\n", [(id)kSecAttrDescription UTF8String]);
printf("%s\n", [(id)kSecAttrComment UTF8String]);
printf("%s\n", [(id)kSecAttrCreator UTF8String]);
printf("%s\n", [(id)kSecAttrType UTF8String]);
printf("%s\n", [(id)kSecAttrLabel UTF8String]);
printf("%s\n", [(id)kSecAttrIsInvisible UTF8String]);
printf("%s\n", [(id)kSecAttrIsNegative UTF8String]);
printf("%s\n", [(id)kSecAttrAccount UTF8String]);
printf("%s\n", [(id)kSecAttrService UTF8String]);
printf("%s\n", [(id)kSecAttrGeneric UTF8String]);
printf("%s\n", [(id)kSecAttrAccessible UTF8String]);
printf("%s\n", [(id)kSecAttrAccessGroup UTF8String]);
printf("%s\n", [(id)kSecAttrCreationDate UTF8String]);
printf("%s\n", [(id)kSecAttrModificationDate UTF8String]);
printf("%s\n", [(id)kSecAttrDescription UTF8String]);
printf("%s\n", [(id)kSecAttrComment UTF8String]);
printf("%s\n", [(id)kSecAttrCreator UTF8String]);
printf("%s\n", [(id)kSecAttrType UTF8String]);
printf("%s\n", [(id)kSecAttrLabel UTF8String]);
printf("%s\n", [(id)kSecAttrIsInvisible UTF8String]);
printf("%s\n", [(id)kSecAttrIsNegative UTF8String]);
printf("%s\n", [(id)kSecAttrAccount UTF8String]);
printf("%s\n", [(id)kSecAttrService UTF8String]);
printf("%s\n", [(id)kSecAttrGeneric UTF8String]);
結果がこれ。
おお、kSecClassは無くなってるわけね。なので追加されてるのは3つ。
kSecAttrAccessible アクセス権源
kSecAttrAccessGroup アクセスグループ
kSecAttrAccount アカウント
kSecAttrAccessGroup アクセスグループ
kSecAttrAccount アカウント
アクセス権源は、そのKeyChain項目にアクセスするのに承認を必要とさせるかとかの設定みたいっす。"ak"とあるけど同じ方法で調べたらkSecAttrAccessibleWhenUnlockedでした。
アクセスグループてのは、複数のアプリケーションで特定のKeyChain項目を共有するための設定で、正しく設定すると別々のiPhoneアプリからアクセスできるようになる。なにも指定しないとtestグループに配属されるみたい。
Appleが用意してくれているサンプルGenericKeychainで実装してくれているんだけど、イントロにちょこっと別アプリケーション間で共有する方法のサンプルとか書いてるんだけで、詳しい説明はGenericKeychain/ReadMe.txt側に書かれてるので、ぱっと見、なんじゃこりゃって思いますな。
ターゲットがGenericKeychain、GenericKeychain2と2つある事自体に気づいてない人多いんじゃねーか?
Appleのサンプルソース:GenericKeychain
アカウントは説明済みですな。
てなところで、今回はこれまで。
------------
サンプルプロジェクト:keychain.zip