Tomute’s Notes Twitter

2009-11-21

[] iPhoneアプリ用のログイン情報設定画面の作り方(その2)

iPhoneアプリ用のログイン情報設定画面の作り方という記事を先日書いたのだが、パスワードをNSUserDefaultsを使用して平文で保存する事はセキュリティの面から好ましくないとの事(Storing passwords in iPhone applications - Stack Overflow)。


上記の記事によると、セキュリティ対策としてKeychainを使う事を薦めているのだが、Appleのドキュメント(Keychain Services Programming Guide: Keychain Services Concepts)にもiPhoneパスワードを保存する場合にはKeychainを使う事が重要であると書かれていた。

ちなみに上記のAppleのドキュメントによると、個々のiPhoneアプリはKeychainにアクセス可能であるが、他のiPhoneアプリのKeychainアイテムにはアクセス出来ないようになっている。また、Keychainに保管されたパスワードiPhoneからPC等にバックアップされないため、他の人に盗まれる危険性を回避出来るとのこと。


では具体的にiPhoneアプリからKeychainを利用する方法であるが、Appleがサンプルコード(GenericKeychain)を提供してくれている。更にBuzz Andersenさんという方がKeyChainにパスワードを保管するためのラッパークラス( SFHFKeychainUtils)を公開してくれているので、今回はこれを使ってパスワード等のユーザー情報を保存する方法を記載する(このクラスはMITライセンス)。


[準備]

その1:ソースコードダウンロード

以下のサイトからソースコードダウンロードし、iPhoneアプリのプロジェクトにコピーする。

必要はファイルはSFHFKeychainUtils.hとSFHFKeychainUtils.mの二つのファイル。

scifihifi-iphone/security at master ? ldandersen/scifihifi-iphone ? GitHub


その2:フレームワークの追加

iPhoneアプリのプロジェクトにSecurity.frameworkを追加する。


以上で準備は終わりである。


[利用方法]

以下のようなログイン情報設定画面で、Saveボタンを押した時にパスワードをKeyChainに保存する場合の例を示す。

f:id:tomute:20091122025818p:image

@interface UserInfoSettingController : UITableViewController <UITextFieldDelegate> {
    UITextField *usernameField;
    UITextField *passwordField;
}

@end

ポイントとしては以下。

  • ユーザ名はNSUserDefaultsも使用して保存し、そのユーザ名をキーにパスワードをKeyChainに保存する
  • 新しいユーザ名でパスワードを入力されたら、古いユーザ名のパスワードはKeyChainから削除する
#import "SFHFKeychainUtils.h"

@implementation UserInfoSettingController
    *snip*
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *CellIdentifier = @"Cell";
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];	
        cell.selectionStyle = UITableViewCellSelectionStyleNone;
        	
        UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(10, 6, 100, 30)] autorelease];
        label.font = [UIFont boldSystemFontOfSize:18];
        [cell.contentView addSubview:label];
        	
        if ([indexPath row] == 0) {
            label.text = @"Username";
            
            usernameField = [[UITextField alloc] initWithFrame:CGRectMake(110, 10, 150, 30)];
            usernameField.returnKeyType = UIReturnKeyDone;
            usernameField.delegate = self;
            usernameField.text = [[NSUserDefaults standardUserDefaults] objectForKey:@"USERNAME"];
            [cell.contentView addSubview:usernameField];
        } else if ([indexPath row] == 1) {
            label.text = @"Password";
            
            NSError *error;
            passwordField = [[UITextField alloc] initWithFrame:CGRectMake(110, 10, 150, 30)];
            passwordField.returnKeyType = UIReturnKeyDone;
            passwordField.delegate = self;
            passwordField.secureTextEntry = YES;
            // ラッパークラスを利用してKeyChainから保存しているパスワードを取得する処理
            passwordField.text = [SFHFKeychainUtils getPasswordForUsername:[[NSUserDefaults standardUserDefaults] objectForKey:@"USERNAME"] andServiceName:@"Test App" error:&error];
            [cell.contentView addSubview:passwordField];
        }
    }

    return cell;
}

- (void)saveUserInfo {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSString *oldUsername = [defaults objectForKey:@"USERNAME"];
    NSError *error;
    if (![oldUsername isEqualToString:usernameField.text]) {
        // ユーザ名が変更になっていた場合は、古いユーザ名で保存したパスワードを削除
        [SFHFKeychainUtils deleteItemForUsername:oldUsername andServiceName:@"Test App" error:&error];
    }
    // ユーザ名はNSUserDefaultsを使って保存
    [defaults setObject:usernameField.text forKey:@"USERNAME"];
    // ラッパークラスを利用してパスワードをKeyChainに保存
    [SFHFKeychainUtils storeUsername:usernameField.text andPassword:passwordField.text forServiceName:@"Test App" updateExisting:YES error:&error];
    [self.navigationItem.rightBarButtonItem setEnabled:NO];
}

- (void)textFieldDidEndEditing:(UITextField *)textField {
    // Saveボタンを有効にする
    [self.navigationItem.rightBarButtonItem setEnabled:YES];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    return YES;
}
    *snip*
@end

[備考]

@takayama さんから教えて頂いて知ったのだが、今回使ったラッパークラス(SFHFKeychainUtils)は、各国別に自分のiPhoneアプリの売り上げを見ることができるAppSaleというiPhoneアプリでも利用されている模様。AppSaleはソースコードが公開されているので、こちらのコードも参考になると思われる(omz/AppSales-Mobile ? GitHub)。


[追記]

SFHFKeychainUtilsのメソッドの引数にはNSErrorを渡すが、そこにnilを渡すと異常終了するので注意が必要である。

またNSErrorを引数に取るにも関わらず戻り値がvoidのため、clangで解析すると警告メッセージが出てしまうという問題有。


masaoikawamasaoikawa 2010/02/05 10:53 有益情報ありがとうございます!

tomutetomute 2010/02/05 11:53 参考になったようで何よりです。

mini37mini37 2011/03/18 12:26 初めまして。いつも参考にさせていただいております。
このプログラムなのですが、
このまま実行すると、
*snip*
の部分がエラーになってコンパイルできません。
始めてまだ1ヶ月程度で、初心者のため、
よろしければご指南いただきたく思います。

tomutetomute 2011/03/18 18:33 mini37さん
*snip*はコードを一部省略しているという意味なので、実際にはそこは削除してもらって結構です。

mini37mini37 2011/03/22 10:35 返信ありがとうございます。もう一度チャレンジしてみます。

tomutetomute 2011/03/22 19:07 頑張ってくださ〜い!

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

リンク元