2010-12-12

キーチェーンサービスを使ってパスワードを保存する

以前の記事で書いたように(→「設定の保存にまつわる問題」)、ユーザのパスワード、それもアプリとは異なる別のサービス用のアカウントのものを、アプリの設定(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 のプロジェクトウィンドウで以下のような操作を行う。

  1. 「グループとファイル」の「Frameworks」グループ上でポップアップメニューを出し「追加」>「既存のフレームワーク...」を選ぶ。
  2. 現れたシートから 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)に組み込むときは、もちろん変更もできるようにしなければならない。

関連リンク

関連記事

0 件のコメント:

コメントを投稿