以前の記事で書いたように(→「設定の保存にまつわる問題」)、ユーザのパスワード、それもアプリとは異なる別のサービス用のアカウントのものを、アプリの設定(User Defaults)にそのまま保存するのは望ましくない。たとえ、アプリの設定(~/Library/Preferences に plist ファイルとして保存される)がバイナリ形式で、かつファイルモードが 0600 であったとしても。
パスワードを保存するなら、やはり「キーチェーン」を使うべきだ。
(「Keychain Services Programming Guide: Introduction」より)
Keychain Services provides secure storage of passwords, keys, certificates, and notes for one or more users. A user can unlock a keychain with a single password, and any Keychain Services–aware application can then use that keychain to store and retrieve passwords.
今回は、このキーチェーンサービスについて調べてみた。
アプリへの組み込み
アプリからキーチェーンサービスにアクセスするために必要な関数は以下の 2 つ。
- SecKeychainAddGenericPassword (キーチェーンにパスワードを追加)
- SecKeychainFindGenericPassword (キーチェーンからパスワードを取得)
これをアプリ内で使う場合の注意点は、(a) security/security.h を import すること、(b) Security.framework をリンクすること、の 2 点だ。(a) はソースに書くだけ。一方、(b) のためには Xcode のプロジェクトウィンドウで以下のような操作を行う。
- 「グループとファイル」の「Frameworks」グループ上でポップアップメニューを出し「追加」>「既存のフレームワーク...」を選ぶ。
- 現れたシートから Security.framework を選び「追加」ボタンを押す。
上記を実行すれば「Frameworks」グループ内に Security.framework が追加されるが、実際にはどこでポップアップメニューを開いても大差はない。それよりも重要なのは「ターゲット」のビルドフェーズ「バイナリをライブラリにリンク」に Security.framework が追加されていることだ。
サンプルアプリ
この 2 つの関数を使って、簡単なサンプルアプリを作ってみた。右のスクリーンショットがそのアプリウィンドウだ。Save Password でアカウントとパスワードを指定して「Save」ボタンを押すと、パスワードがキーチェーンに保存される。次に、Load Password でアカウントを入力し「Load」ボタンを押すと、下のパスワードフィールドに保存したパスワードが表示される。もちろん、アカウントが間違っていればパスワードの取得に失敗する。
デフォルトのキーチェーンがロックされていれば、保存時にロックを解除するかというダイアログが出てくるし、同様に取得時にはキーチェーンへのアクセスを許可するかというダイアログも出てくる。また、実際にパスワードが保存できていることは「キーチェーンアクセス.app」を起動すれば確認できる。
以下に、アプリのソースの一部(AppController.{h,m})を示す。
#import <Cocoa/Cocoa.h> @interface AppController : NSObject { NSTextField *accountFieldForSave; NSTextField *passwordFieldForSave; NSTextField *accountFieldForLoad; NSTextField *passwordFieldForLoad; } @property (assign) IBOutlet NSTextField *accountFieldForSave; @property (assign) IBOutlet NSTextField *passwordFieldForSave; @property (assign) IBOutlet NSTextField *accountFieldForLoad; @property (assign) IBOutlet NSTextField *passwordFieldForLoad; - (IBAction)load:(id)sender; - (IBAction)save:(id)sender; @end
#import <Security/Security.h> #import "AppController.h" NSString * const KCTServiceName = @"KeyChainTest App"; @implementation AppController @synthesize accountFieldForSave, passwordFieldForSave; @synthesize accountFieldForLoad, passwordFieldForLoad; - (IBAction)load:(id)sender { NSString *account = [accountFieldForLoad stringValue]; const char *serviceName = [KCTServiceName UTF8String]; const char *accountName = [account UTF8String]; void *passwordData = nil; SecKeychainItemRef itemRef = nil; UInt32 passwordLength; OSStatus status; status = SecKeychainFindGenericPassword(NULL, strlen(serviceName), serviceName, strlen(accountName), accountName, &passwordLength, &passwordData, &itemRef); NSLog(@"Load password: status = %d", status); if (status == noErr) { NSString *password = [[NSString alloc] initWithBytes:passwordData length:passwordLength encoding:NSUTF8StringEncoding]; [passwordFieldForLoad setStringValue:password]; status = SecKeychainItemFreeContent(NULL, passwordData); [password release]; } else { [passwordFieldForLoad setStringValue:@""]; } } - (IBAction)save:(id)sender { NSString *account = [accountFieldForSave stringValue]; NSString *password = [passwordFieldForSave stringValue]; // !!!: const char *serviceName = [KCTServiceName UTF8String]; const char *accountName = [account UTF8String]; const char *passwordData = [password UTF8String]; OSStatus status; status = SecKeychainAddGenericPassword(NULL, strlen(serviceName), serviceName, strlen(accountName), accountName, strlen(passwordData), passwordData, NULL); NSLog(@"Save password: status = %d", status); } @end
Cocoa のオブジェクトにくるまれていないサービスのため、使うのが少し面倒(NSString を直接わたせなかったり)だが、実質的に 2 つの関数を呼び出すだけでパスワードのような機密性の高い情報を安全に保管できる。やってみると思ったよりも簡単だった。
実は、この 2 つの関数だけではパスワードを変更することができない(同じアカウントで Add を呼ぶとエラーになる)。実際のアプリ(MacBloggerGlass)に組み込むときは、もちろん変更もできるようにしなければならない。
関連リンク
- Keychain Services Programming Guide (Mac OS X Reference Library)
0 件のコメント:
コメントを投稿