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

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


上記の記事によると、セキュリティ対策としてKeychainを使う事を薦めているのだが、Appleのドキュメント(Keychain Services | Apple Developer Documentation)にも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に保存する場合の例を示す。

@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はソースコードが公開されているので、こちらのコードも参考になると思われる(GitHub - omz/AppSales-Mobile: App Sales allows iPhone and Mac App Store developers to download and analyze their daily and weekly sales reports from iTunes Connect.)。


[追記]
SFHFKeychainUtilsのメソッドの引数にはNSErrorを渡すが、そこにnilを渡すと異常終了するので注意が必要である。
またNSErrorを引数に取るにも関わらず戻り値がvoidのため、clangで解析すると警告メッセージが出てしまうという問題有。