CoreData - マイグレーションを考慮した CoreDataManager パターン

2011年2月26日土曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

2/27 マイグレーションにかかる時間を訂正・加筆しました(別の数字を誤って掲載してました。実際はもっと遅い)。

一般的な CoreDataManager パターンの問題


CoreData を使う場合 CoreDataManager というシングルトンを使って NSManagedObjectContext などを管理させるのが一般的なパターン。こんな感じ。
@interface CoreDataManager : NSObject {

 // Core Data Stack
 NSPersistentStoreCoordinator *persistentStoreCoordinator_;
  NSManagedObjectModel *managedObjectModel_;
  NSManagedObjectContext *managedObjectContext_;
}
@property (nonatomic,retain, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator;
@property (nonatomic,retain, readonly) NSManagedObjectModel* managedObjectModel;
@property (nonatomic,retain, readonly) NSManagedObjectContext* managedObjectContext;

+ (CoreDataManager*)sharedManager;
利用側のコードでは必要な時にここから NSManagedObjectContext を取り出せば良い。
NSManagedObjectContext* moc = [[CoreDataManager sharedManager] managedObjectContext];

ただこれだとエンティティに変更を入れてマイグレーションが必要になった時に困ることがある。例えば起動直後の画面で CoreData へアクセスするような構成で起動時にマイグレーションが発生した場合、起動ルール(20秒)にかかりアプリが起動に失敗するというケースがある。

Cocoaの日々: [iOS] 起動に時間がかかりすぎるとクラッシュする(原因と対策など)

CoreData のマイグレーションは非常に遅くて下図ぐらいのエンティティ構成でレコードが 1,200件程度(親テーブル 100件、子テーブル 12件)のデータの場合、40〜50秒 2分かかる(iOS 4.2.1/3GS、自動マイグレーション、変更はカラム2個追加)。

2/26追記:マイグレーションにかかる時間(上記と同条件)
240件  28秒
600件  1分
1,200件 2分
2,400  2分20秒
4,800  9分
※ケーブルで MacBookに接続した iPhone 3GS に Xcode経由で実行(デバッガ未使用)。


CoreData を使うアプリであればこの程度の件数はすぐに行くので、起動時にマイグレーションが走ると確実に落ちてしまう。これを防ぐためには起動時に CoreData へアクセスさせないのが最低限の対策になるが、その場合でもユーザが CoreData へアクセスする操作を行った瞬間にマイグレーション処理に時間がかかって画面が固まったようになるのでユーザビリティは良くない。


マイグレーションを考慮したパターン


よって CoreDataを使うアプリではマイグレーション用の画面を用意するのがベスト。処理フローはこんな感じ。
èµ·å‹•
 ↓
(1)マイグレーションチェック
 もし必要なら、マイグレーション用の画面へ遷移し、(2)マイグレーション実行
 ↓
通常画面
マイグレーションチェックは NSPersistentCoordinator を使えばわかる。

Cocoaの日々: [iOS][Mac] CoreData - マイグレーションが必要かどうかを知る

以下は実際に上記パターンを適用した時の画面例。
アプリ起動後にマイグレーションが必要と判断したら専用の画面をモーダル表示させる。
if ([manager isRequiredMigration]) {
   // do migration
   MigrationViewController* viewController = [[MigrationViewController alloc] init];
   viewController.rootViewController = self;
   viewController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
   [self presentModalViewController:viewController animated:NO];
   [viewController release];
 :
表示した画面の中でマイグレーションを実行する。画面には UIActivityIndicatorView を表示して回しておく。iOS 4 以降であれば GCD が使えるのでこの辺りの処理は簡単に書ける。
- (void)viewWillAppear:(BOOL)animated
{
 dispatch_queue_t queue =
  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

 self.okButton.hidden = YES;
 self.buttonLabel.hidden = YES;
 self.indicator.hidden = NO;
 self.warningLabel.hidden = NO;
 
 dispatch_async(queue, ^{
  [[CoreDataManager sharedManager] doMigration];

  dispatch_async(dispatch_get_main_queue(), ^{
   self.message.text = @"バージョンアップが完了しました";
   self.indicator.hidden = YES;
   self.okButton.hidden = NO;
   self.buttonLabel.hidden = NO;
   self.warningLabel.hidden = YES;
  });
 });

}
マイグレーションが終わったらOKボタンを表示する。ここはアプリによってはそのまま通常画面へ遷移しても良い。


上記のように CoreDataManager にはマイグレーション用のメソッドが必要。こんな感じ。
@interface CoreDataManager : NSObject {

 // Core Data Stack
 NSPersistentStoreCoordinator *persistentStoreCoordinator_;
  NSManagedObjectModel *managedObjectModel_;
  NSManagedObjectContext *managedObjectContext_;
}
@property (nonatomic,retain, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator;
@property (nonatomic,retain, readonly) NSManagedObjectModel* managedObjectModel;
@property (nonatomic,retain, readonly) NSManagedObjectContext* managedObjectContext;

+ (CoreDataManager*)sharedManager;

// for migration
- (BOOL)isRequiredMigration;
- (BOOL)doMigration;
自動マイグレーションを使っている場合、マイグレーションが発生するタイミングは NSpersistentCoordinator にマイグレーションオプションを設定した上で -addPersistentStoreWithType:configuration:URL:options:error: を投げた時になる。以下は一般的な -psersistentStoreCoordinator のコード例。
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
 if (persistentStoreCoordinator_ != nil) {
        return persistentStoreCoordinator_;
    }

    persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc]
          initWithManagedObjectModel:self.managedObjectModel];
 
 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
        [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
        [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
  
    NSError *error = nil;
 NSURL* fileURL = [CoreDataManager fileURL_]; 
    if (![persistentStoreCoordinator_ addPersistentStoreWithType:NSSQLiteStoreType
              configuration:nil
               URL:fileURL
              options:options
                error:&error]) {
  NSLog(@"Creating persistentStoreCoordinator was failed (%@, %@)", error, [error userInfo]);
  abort();
    }

    return persistentStoreCoordinator_;
}
NSMigratePersistentStoresAutomaticallyOption と NSInferMappingModelAutomaticallyOption がマイグレーションオプション。通常の -persistentStoreCoordinator メソッドではこのオプションを設定せず、別途用意する -doMigration でマイグレーション専用の NSPersistentCoordinator を用意しそこでオプションを設定してマイグレーションを実行(-addPersistentStoreWithType:configuration:URL:options:error:)すれば良い。マイグレーション実行後はここで作った NSPersistentCoordinator は捨てて良い(NSPersistentCoordinator は同じDBファイルに対して複数作成可能)。

なお CoreData へのアクセスタイミングをきちんと制御できるのであれば別途 -doMigration を用意せず既存のメソッドだけでも良い( [[CoreDataManager sharedManager] managedObjectContext] が最初に呼ばれたタイミングで -psersistentStoreCoordinator も呼ばれるのでそこでマイグレーションが走る)。

- - - -
CoreData を扱う上での一番のネックはマイグレーションに時間がかかること。たった一つのカラム追加でも現状は全データの移行作業が発生するので(特に iOSでは)この時間が馬鹿にならない。CoreDataはプログラミング的には非常に便利なのだがこの運用時のマイグレーション処理が少々厄介。アプリで CoreData を採用するかどうか判断する時にはこの点を考慮した方が良いかと思う。


参考情報


Cocoaの日々: [iOS] CoreData - マイグレーション[5] マイグレーション中にアプリを終了させたらどうなる?

[iOS] iPad の場合、UITableView の背景を透明にするには setBackgroundView:nil が必要

| Published in | 0 コメント

このエントリーをはてなブックマークに追加

元ネタ。
setBackgroundView:nil


self.tableView.backgroundColor = [UIColor clearColor];
iPhone の時にやっていた上記が効かず困っていたのだが、そういことか。

- (void)viewDidLoad {
    [super viewDidLoad];

 [self.tableView setBackgroundView:nil];
 [self.tableView setBackgroundView:[[[UIView alloc] init] autorelease]];
 self.tableView.backgroundColor = [UIColor clearColor];
透明になった。

[iOS][Mac] CoreData - マイグレーションが必要かどうかを知る

2011年2月24日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

マイグレーションの要不要は?


CoreData では属性を追加したり変更するとマイグレーションが必要になる。過去にリリースしたアプリを新しいアプリでバージョンアップする時にマイグレーションが必要かどうか判断するにはどうしたらよいか?


実装


NSPersistentStoreCoordinator を使えば良い。
こんな感じ。
- (BOOL)isRequiredMigration
{
 CoreDataManager* manager = [CoreDataManager sharedManager];
 [[[NSPersistentStoreCoordinator alloc]
  initWithManagedObjectModel:manager.managedObjectModel] autorelease];
 
 NSURL* fileURL = [CoreDataManager fileURL_];
 NSError* error = nil;
 
 NSDictionary* sourceMetaData =
  [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                   URL:fileURL
                 error:&error];
 
 if (sourceMetaData == nil) {
  return NO;
 } else if (error) {
  NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
  abort();
 }
 

 BOOL isCompatible = [manager.managedObjectModel isConfiguration:nil
          compatibleWithStoreMetadata:sourceMetaData]; 

 return !isCompatible;
 
}
古いバージョンのアプリで使用しているメタデータと、(これから実行しようとしている)新しいアプリで使用するメタデータの比較を行えば良い。



- - - -
NSPersistentStoreCoordinator のインスタンスは1つしか作れないという制限はなく必要ならいくつでも作ることができる。これは同じDBファイルに対してもそう(ただし非スレッドセーフなので排他制御は自分で行う必要がある。その為のメソッドも用意されている)。
上記の様にマイグレーションの要不要をチェックする為に NSPersistentStoreCoordinator のインスタンスを作ることは、複数インスタンスを作る場合の利点の一つ。普通に1つの NSPersistentStoreCoordinator インスタンスだけで通常の利用とマイグレーションを一緒にやろうとした場合、起動時にDBへアクセスしようとした時に時間のかかるマイグレーションが発生して起動時間問題にひっかかるなどの弊害が出る場合がある。
他には大量のインポートを行いたい時に専用の NSPersistentStoreCoordinator インスタンスを用意する、複数のスレッドでインスタンスを持つ、などが考えられる。SQLite に対するオプションは NSPersistentStoreCoordinator の単位で設定できるので、インポートなど特殊な用途向けのオプションを設定する場合などにインスタンスを分けることが役立つ。

[iOS] iPad へクライアント証明書をインストールしたときのスクリーンショット

2011年2月22日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

今後、開発中の iPad/iPhone アプリでクライアント証明書を使用する可能性があるので少しづつ検証を進めていく。今回は試しに PKCS#12形式(拡張子 p12)の証明書ファイルをメールで添付し、それを iPad の Mail.app で開いてインストールした。その時のスクリーンショットを記録として残しておいた。なおインストールしたクライアント証明書は、その証明書を要求するサイトへのアクセスで利用することが確認できた。

わかったこと
・iOS 4.2 へ PKCS#12(拡張子 p12)形式のファイルのインストールが可能
・インストールした証明書は Safari で利用可能(自動的に利用される)


以下、スクリーンショット。


メールに添付されたファイルをタップ


「設定」が開き、インストールボタンが表示される。

詳細

インストールボタンを押すと確認を求められる。

iPad のパスコードが求められる。

証明書のパスワードを入力

インストール完了。「信頼されていない」と表示される。
この後、メールソフトへ制御が戻る。

インストールした 証明書は「設定」>「一般」>「プロファイル」で確認できる。

この後、この証明書を必要とするサイトへSafari でアクセスしたところ接続に成功した。証明書の利用を確認するダイアログは何も表示されない(自動的に使われている)。この辺りの挙動は Mac OS X 版 Safari と同じ。

- - - - -
標準的な仕組みは理解できた。実際に使う場合、メールで証明書を送るのはまずいのでアプリ内で証明書をインストールする仕組みが必要だと思われる。その為のAPIも用意されているはずなので、これは今後の課題。

毎日更新終了のお知らせ

2011年2月18日金曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

ブログはこの先も続けますが、今後の更新頻度は毎日ではなく不定期にします。

理由はご存知のとおり(?)、ここのところ実質毎日更新ができていないことにあります。最近は子育て他で以前に比べるとプログラミングに割ける時間がぐっと減ってしまい、ブログを書く時間もなかなか取れなくなってきました。またブログを書く為にアプリ開発に時間が取れないという本末転倒な状況に陥ってきたというのもあります。

今年の2月で毎日更新が満3年を迎えてちょうど節目を迎えたことと(*1)、毎日更新を始めた当初の目的である「Cocoaの勉強を習慣化する」というのが果たせたということもあり、このタイミングで毎日更新を終了することにしました。

ブログを続けることで SimpleCap を完成することできたし今では iPhone/iPad アプリ開発の仕事もするようになって Cocoa を使う機会も増えました。そういう意味でもブログを続けて時間を費やしてきたことは無駄じゃなかったと思ってます。


更新頻度は低くなりますが
今後もまたよろしくお願いします。

Hiroshi Hashiguchi (@xcatsan)


(*1) 2008å¹´2月から開始、今年までに書いたエントリは 1,100ぐらい。

[iOS] 複数アプリケーション間でのデータ共有 〜 Keychain Services を使った第三の方法

2011年2月7日月曜日 | Published in | 1 コメント

このエントリーをはてなブックマークに追加

※タイトルはあまり深い意味は無い。なんとなく「第三の〜」の響きが良かったので。。

前回紹介した Keychain Services を使えば制限付きながら iOS 上の複数のアプリケーションでデータ共有ができることがわかったのでそれを解説する。

[前回] Cocoaの日々: [iOS] Keychain Services とは


仕組み


Keychain Services に格納されるアイテム(パスワードなど)のアクセス制御は Keychain Access Group(グループ)を元に行われる。アイテムにはこのグループ属性があり、同じグループに所属しているアプリケーションからのみアクセスが許可される。

アプリケーションは複数のグループに所属することができるので、データの共有を目的したグループを用意しておき、複数のアプリケーションでこのグループに所属すれば、このグループに所属するアイテムへそれら複数のアプリケーションがアクセスすることができる。

Keychain Services のアイテムはパスワード、秘密鍵、証明書を格納するようになっているが、CFData(NSData)åž‹ であればパスワードである必要はない。格納したい値を CFData(NSData)へ変換すれば Keychain Services へ格納することができるので任意の値を共有することができる。

Keychain Access Group の詳細は前回の「3. アクセス制御」を参照のこと。


サンプル


プロジェクトを2つ(KeyChainApp-1と KeyChainApp-2)用意し、それぞれのアプリケーションから同じアイテムへアクセスできるかを検証してみた。


KeyChainApp-1


Entitlements.plist

application-identifier: GFDZH8PXXX.com.yourcompany.KeyChainApp-1

アイテム登録コード
- (IBAction)addNewItem
{
 NSData* passwordData = [self.password.text dataUsingEncoding:NSUTF8StringEncoding];

 NSMutableDictionary* attributes = [NSMutableDictionary dictionary];
 [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [attributes setObject:(id)self.account.text forKey:(id)kSecAttrAccount];
 [attributes setObject:passwordData forKey:(id)kSecValueData];
 [attributes setObject:@"GFDZH8PXXX.share" forKey:(id)kSecAttrAccessGroup];

 OSStatus err = SecItemAdd((CFDictionaryRef)attributes, NULL);
 if (err == noErr) {
  NSLog(@"SecItemAdd: noErr");
 } else {
  NSLog(@"SecItemAdd: error(%d)", err);
 }
}
登録するアイテムの Keychain Access Group (kSecAttrAccessGroup)に "GFDZH8PXXX.share" を指定している。
※サンプルコードを自分の環境でビルドする場合は "GFDZH8PXX" の箇所を自分のプロビジョニングファイルの app-identifier に書き換えること。

KeyChainApp-2


Entitlements.plist

application-identifier: GFDZH8PXXX.com.yourcompany.KeyChainApp-2

アクセス可能なアイテムをすべてデバッグコンソールへ表示。
- (IBAction)dumpItems
{
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
 [query setObject:(id)kSecMatchLimitAll forKey:(id)kSecMatchLimit];
 
 CFArrayRef result = nil;
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query,(CFTypeRef*)&result);
 
 if (err == noErr) {
  NSLog(@"SecItemCopyMatching: noErr");
  NSLog(@"%@", result);
 } else if(err = errSecItemNotFound) {
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
}
パスワードを更新。
- (IBAction)updateItem
{
 NSMutableDictionary* attributes = nil;
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 NSData* passwordData = [self.password.text dataUsingEncoding:NSUTF8StringEncoding];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)self.account.text forKey:(id)kSecAttrAccount];
 
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL);
 
 if (err == noErr) {
  // update item
  NSLog(@"SecItemCopyMatching: noErr");
  
  attributes = [NSMutableDictionary dictionary];
  [attributes setObject:passwordData forKey:(id)kSecValueData];
  
  err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes);
  if (err == noErr) {
   NSLog(@"SecItemUpdate: noErr");
  } else {
   NSLog(@"SecItemUpdate: error(%d)", err);
  }
  
 } else if (err = errSecItemNotFound) {
  // add new item
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
  
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
 
}


結果


まず KeyChainApp-1 を立ち上げてアカウント/パスワードを登録(Add new item)する。
Dump items で登録内容を確認しておく。
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3030>;
    }
続いて KeyChainApp-2 を立ち上げ、登録アイテムを表示する(Dump items)。

すると
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3030>;
    }
出た。KeyChainApp-1 で登録したデータをまったく別のアプリ KeyChainApp-2 で読み出すことができた。

更新はどうだろうか。KeyChainApp-1 で登録したこのデータ(パスワード)ã‚’ KeyChainApp-2 で書き換えてみる。
すると
{
        acct = hashiguchi;
        agrp = "GFDZH8PCUM.share";
        pdmn = ak;
        svce = "";
        "v_Data" = <70617373 3131>;
    }
書き換わった。


ソースコード


GitHub からどうぞ。
KeyChainApp-1 at 2011-02-07 from xcatsan/iOS-Sample-Code - GitHub
KeyChainApp-2 at 2011-02-07 from xcatsan/iOS-Sample-Code - GitHub


制限


複数アプリケーション間でのデータ共有方法として使えることがわかった Keychain Services だが重大な制限もある
同じプロビジョニングファイルから作成されたアプリケーション間でしか
Keychain Services を介したデータ共有は行えない
これは Keychain Access Group の指定方法の制約による。詳細は前回の「3. アクセス制御」を参照のこと。この為、残念ながら他社の作成したアプリとデータ交換を自由に行えるわけではない。


補足


シミュレータの場合、Entitlements.plist は無視されるので今回のように kSecAttrAccessGroup を設定すると登録はエラーとなる(-25243)。シミュレータの場合、前回説明したように Keychain Access Group は常に "test"(固定)となる。

[iOS] Keychain Services とは

2011年2月6日日曜日 | Published in | 2 コメント

このエントリーをはてなブックマークに追加

パスワードを暗号化して安全に iPhone/iPad へ保管したい。iOS はこの用途の為に Keychain Services を提供している。今回は Keychain Services について調べてみた。リファレンスの内容に加え、独自に調査・検証した結果をまとめてある。動作確認の為のサンプルも GitHub に置いておいた。

  1. 概要
  2. 利用方法
    2.1 API
    2.2 検索条件(query)
    2.3 属性値(attributes)
    2.4 クラス
    2.5 属性の種類
    2.6 ユニークキー
    2.7 kSecAttrAccessible
    2.8 エラーコード
  3. アクセス制御
    3.1 Keychain Access Group(グループ)
    3.2 アプリケーションの所属グループ
    3.3 グループの変更
    3.4 グループの命名ルール
    3.5 デフォルトグループの決定ルール
    3.6 アイテム登録時に指定可能なグループ
    3.7 シミュレータにおけるグループ
    3.8 グループ設定の例
  4. 開発・運用情報
    4.1 API 利用方法
    4.2 実機とシミュレータでの違い
    4.3 アプリを削除した場合の挙動
  5. サンプル
    5.1 登録・æ›´æ–°
    5.2 削除
    5.3 検索(パスワード取得)
    5.4 検索(全アイテム取得)
  6. 運用上の注意点
  7. ソースコード
  8. 参考情報
  9. 付録

なお iOS で Keychain Services を使用する場合、いくつか重要な注意点がある。それらは「6. 運用上の注意点」に簡単にまとめたので参照されたい。


1. 概要


Keychain Services はパスワードや秘密鍵、証明書などを保存する安全なストレージとそれを操作するAPIを提供している。ストレージは暗号化されていてパスワード(keychain password)によって保護されている。このパスワードが無い限りストレージ内の情報を利用(復号化)することはできない。Keychain Services では格納しているパスワードや秘密鍵、証明書の情報をアイテムと呼んでいる。Keychain Services はこれら複数のアイテムを管理し、登録、変更、削除、検索するための API を提供している。


Mac OS X、iOS 共にこのサービスはサポートされているが、プラットフォームによって使い方が若干異なる。

プラット
フォーム
他アプリの
情報へ
アクセス
利用時パスワード要求
(keychain password)
パスワード指定方法
(keychain password)
主に使用するAPI
Mac OS X求められる
※設定による
ユーザ指定SecKeychain系
iOS求められないシステム自動生成SecItem系

他アプリケーションが格納した Keychain Services 内の情報へのアクセス


Mac OS X の場合はユーザが許可を与えれば他のアプリケーションの情報へアクセスすることができる。一方、iOS の場合、アプリケーションは自身が保存した情報のみアクセスが行える。他のアプリケーションの情報へは基本的にアクセスすることができない。ただし同じプロビジョニングプロファイルを使ってビルドされたアプリは設定により情報を共有することができる(後述)。

iOS での特記事項


  • iOS には単一のキーチェーンのみ存在する(Mac OS X は複数)。
  • iOS の場合、PC接続時にストレージの内容は暗号化されたままバックアップされる。これを復号化するパスワード(keychain password)はバックアップされない(iOSデバイスの中から外に持ち出されない)。
  • Keychain Service はプロビジョニングファイルの情報を利用する。この為、アプリケーションをバージョンアップする場合、同じプロビジョニングファイルを使うことが推奨される。[*1]
[*1] アクセス権限を決定する Keychain Access Group の値にデフォルトでプロビジョニングファイルで定義される App Identifier が使用される為。


2. 利用方法


2.1 API


Security.framework で提供される下記の4つのAPIを使用して Keychain Services へアクセスする。
SecItemAdd         (CFDictionaryRef attributes, CFTypeRef* result);
SecItemUpdate      (CFDictionaryRef query, CFDictionary attributes);
SecItemDelete      (CFDictionaryRef query);
SecItemCopyMatching(CFDictionaryRef query, CFTypeRef* result);
Keychain Services Reference - Functions

2.2 検索条件(query)


APIの引数 query は、Keychain Services 内のアイテムを検索したり、更新対象のアイテムを指定する時の条件として使用する。削除する場合の削除対象のアイテムもこの query で特定する。
例)CFDictionaryRef query
      |--kSecClass            = kSecClassGenericPassword // パスワードクラスを指定
      |--kSecAttrAccount      = @"hashiguchi"            // アカウント
      |--kSecRetrunAttributes = kCFBooleanTrue           // 結果を CFDictionary型で受け取る
上記を SecItemCopyMatchingの第一引数へ渡すと、第二引数で結果を受け取ることができる。また SecItemUpdate での更新対象の指定、SecItemDelete での削除対象を指定するのにも使われる。

検索で使えるキーは次のようなものがある。
Search Keys

キー内容
kSecMatchPolicySecPolicyRefを条件とする
※証明書で使用
kSecMatchItemList指定した配列を検索対象にできる。また persistent reference から normalreference への変換や persistent reference を指定した削除などの用途でも利用。
kSecMatchSearchList※不明(リファレンスに説明なし)。kCFBooleanTrue を設定してみたが結果に変化なし。
kSecMatchIssuersX.500 Issuer(証明書発行者)を配列で指定する
※証明書で使用
kSecMatchEmailAddressIfPresentRFC822で定義されるemailを指定する
※証明書で使用
kSecMatchSubjectContainsX.500 Subject(証明書取得者)の部分一致文字列を指定する
※証明書で使用
kSecMatchCaseInsensitive文字列比較時の大文字小文字区別の有無
kSecMatchTrustedOnly信頼できる認証局に発行されている証明書かどうかを指定
※証明書で使用する
kSecMatchValidOnDate証明書の有効期限を指定
※証明書で使用する
kSecMatchLimit結果で一度に取得する最大件数を指定
kSecMatchLimitOne結果1件のみを取得する(デフォルト動作)
kSecMatchLimitAll結果すべてを取得する(件数無制限)

検索結果で受け取りたい値の種類・型をあらかじめ query に設定しておく。設定可能なキーは次の通り。
キー型値結果例(NSLog出力例)
kSecReturnDataCFDataRefパスワード<62706c69 73743030 ...
kSecReturnAttributesCFDictionaryRef属性値{
acct = hashiguchi;
agrp = test;
gena = <70617373 776f7264>;
pdmn = ak;
svce = SampleService;
}
kSecReturnRef下記のいずれか(クラスに依存)
SecKeychainItemRef
SecKeyRef
SecCertificateRef
SecIdentityRef
CFDataRef
※クラスに依存※kSecClassGenericPasswordでは戻り値なし
kSecReturnPersistentRefCFDataRefディスク上に格納されたオブジェクトへの参照、もしくは別プロセスとの共有オブジェクトへの参照を表す(※詳細未調査)。<67656e70 00000000 00000001>
上記キーに値 CFBooleanTrue を指定すると有効になる。なお複数指定した場合、すべての結果が CFDictionaryRef に格納されて戻される。
(例)
    {
        acct = ddddd;
        agrp = test;
        class = genp;
        pdmn = ak;
        svce = "";
        "v_Data" = <64>
        "v_PersistentRef" = <67656e70 00000000 00000018>;
    }
"v_Data" が kSecReturnData に、"v_PersistentRef" が kSecReturnPersistentRef に、それ以外は kSecReturnAttributes が対応している。


2.3 属性値(attributes)


APIの引数 attributes は、新規登録や更新するアイテムの情報を指定する用途で使用する。検索結果も同じ形式で受け取る。
例)CFDictionaryRef attributes
      |--kSecClass            = kSecClassGenericPassword // パスワードクラスを指定
      |--kSecAttrAccount      = @"hashiguchi"            // アカウント
      |--kSecValueData        = [@"Jusdf087" dataWithEncoding:NSUTF8StringEncoding]  // パスワード
      |--kSecAttrDescription  = @"Item description"
      |--kSecAttrService      = @"Service"
      |--kSecAttrComment      = @"Your comment here."
上記を SecItemAdd の第一引数へ渡すとその内容が Keychain Services へ登録される。保存したい情報(パスワードや証明書)は kSevValueData をキーにして NSData型で渡す。既に同じアカウントが存在する場合は重複エラー(-25299:errSecDuplicateItem)となる。

2.4 クラス


アイテムの種類は kSecClass 属性で指定する。この種類のことをクラスと呼び、クラスは次の5種類が用意されている。
kSecClassGenericPasswordパスワード
kSecClassInternetPasswordインターネットパスワード
kSecClassCertificate証明書
kSecClassKey暗号鍵(秘密鍵、公開鍵など)
kSecClassIdentity秘密鍵付き証明書
Keychain Services へ格納するアイテムはこのいずれかのクラスに分類する。一般的なアプリケーションでは kSecClassGenericPassword が使われると思われる。

2.5 属性の種類


設定可能な属性の種類は用途に応じて様々なものが用意されている。
Attribute item

これら全てが使われるわけではなく、クラスによって使う属性値が決まっている。例えば kSecClassGenericPassword の場合、次の属性値を扱うことができる。
キー意味値の型
kSecAttrAccessibleアクセス制約オプションCFTypeRef
kSecAttrAccessGroupアクセスグループCFStringRef
kSecAttrCreationDateアイテム作成日CFStringRef
kSecAttrModificationDateアイテム更新日時CFStringRef
kSecAttrDescriptionアイテムの説明CFStringRef
kSecAttrCommentコメントCFStringRef
kSecAttrCreator作成アプリ(4文字)CFNumberRef
kSecAttrTypeアイテムタイプ(4文字)CFNumberRef
kSecAttrLabelユーザへ表示する文字列(ラベル)CFStringRef
kSecAttrIsInvisible不可視属性(利用はアプリ次第)CFStringRef
kSecAttrIsNegative無効属性(利用はアプリ次第)CFStringRef
kSecAttrAccountアカウント(ログインIDなど)CFStringRef
kSecAttrServiceサービス名(Application Identifierなど)CFStringRef
kSecAttrGeneric利用目的が自由な情報CFDataRef
上記は基本的にプログラムで設定する必要がある。例えばアイテム作成日時 kSecAttrCreationDate は自動的に設定される訳ではない。なお kSecAttrAccessGroup は指定がない場合、自動的にデフォルト値が設定される(後述)。

2.6 ユニークキー


検索時にある1つのアイテムを特定するキー(ユニークキー)は kSecClassGenericPassword の場合、次の2つで構成されている [*1]
  • kSecAttrAccount
  • kSecAttrService
[*1] ドキュメントに書かれていたわけではなく調査の結果そう判断した。

kSecAttrService の値が異なれば同じ kSecAttrAccount の値をもつアイテムを登録することができる。また kSecAttrService は登録時に省略することができて、kSecAttrAccount だけで運用することもできる。その場合は同じ kSecAttrAccount の値は複数登録できない(登録時に重複エラーとなる)。

Keychain Services Programming Guide のサンプルなどで設定されている kSecAttrGeneric はユニークキーの構成には入らない。この為、kSecAttrAccount の値が同じで、kSecAttrGeneric の値が異なるアイテムの登録はできない(やはり重複エラーとなる)。ただし検索キーとしては有効に働くので、プログラム内でのキーの用途や分類を表す検索タグのような用途で利用することができる。

2.7 kSecAttrAccessible


kSecAttrAccessible属性を設定することで Keychain Services 内に格納した情報へのアクセスに制限を加えることができる。
キーアクセス条件推奨用途リストアの可否
kSecAttrAccessibleAfterFirstUnlock再起動後最初のアンロック以降
次の再起動まで
バックグラウンド
アプリケーション
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly再起動後最初のアンロック以降
次の再起動まで
バックグラウンド
アプリケーション
×
kSecAttrAccessibleAlwaysなし
(常にアクセス可)
アプリケーションでの
利用は推奨されない
kSecAttrAccessibleAlwaysThisDeviceOnlyなし
(常にアクセス可)
アプリケーションでの
利用は推奨されない
×
kSecAttrAccessibleWhenUnlockedデバイスが
アンロックされた状態
フォアグラウンド
アプリケーション
kSecAttrAccessibleWhenUnlockedThisDeviceOnlyデバイスが
アンロックされた状態
フォアグラウンド
アプリケーション
×
  • デバイスの状態(ロック、アンロック)の他、PCへバックアップしたデータをリストアする時の対象に含めるかどうかを指定することができる。
  • デフォルト(未指定時)は kSecAttrAccessibleWhenUnlocked となっている。バックグラウンドで動作するアプリの場合、ロック状態(スリープ時)にはアクセスできないので注意。ロック状態でも利用したい場合は kSecAttrAccessibleAfterFirstUnlock を明示的に指定する。

2.8 エラーコード


API の戻り値(エラーコード)はリファレンスで定義されている。
Keychain Services Reference
各APIの戻り値は、0 (errSecSuccess) なら正常終了、それ以外はエラー。


3. アクセス制御


3.1 Keychain Access Group


Keychain Services で扱われるアイテムは属性 kSecAttrAccessGroup の値によってアクセスの可否が制御されている。この属性値で示される文字列を Keychain Access Group(以下、グループ)と呼ぶ。アプリケーションはアクセス対象のアイテムと同じグループに所属している必要がある。アプリケーションと異なるグループに所属しているアイテムは検索にヒットしない。例えばあるアイテムの属性 kSecAttrAccessGroup に "GFDZH8DCX.com.xcatsan.keyChainsSample" と設定されている場合、このアイテムへアクセスすることができるアプリケーションはグループ "GFDZH8DCX.com.xcatsan.keyChainsSample" に所属している必要がある。

イメージ:

3.2 アプリケーションの所属グループ


通常アプリケーションは application-identifer で定義されるグループに所属している。application-identifier はデフォルトでは $(AppIdentifierPrefix)$(CFBundleIdentifier) として定義される。$(AppIdentifierPrefix) はプロビジョニングファイルの App Identifier のアスタリスク"*"の左側の英数字、$(CFBundleIdentier)は Info.plist で定義されているアプリケーション識別子を指す。

application-identifier の例
GFDZH8DCX.com.xcatsan.keyChainsSample
プロビジョニングプロファイルの App Identifier の例
アイテムを登録する時にはデフォルトでアプリケーションが所属する application-identifer が kSecAttrAccessGroup に設定される。この為、アプリケーション自身が登録したアイテムを後から読み出すことができる。

なお後述する Entitlements.plistファイルの作成するとこのデフォルト値を変えることができる。

3.3 Keychain Access Group の変更


Keychain Access Group のデフォルト値は application-identifier が使われるが、Entitlements.plistファイル[*1] ã‚’用意することで Keychain Access Group のデフォルト値をアプリケーションで変えることができる。またこのファイルにグループを複数既述することで、アプリケーションが所属するグループを複数定義することができる。以下、plistファイルの作成手順。
[*1] plistファイルの名前は自由に付けることができてリファレンスでは例として keychain-access-groupslplist が上げられていた。ただコード署名の用途などでこのファイルを作成するケースもあり、Keychain Access Group 専用の設定ファイルというわけではない。この為、ここでは独自のファイル名ではなく標準的に使われる Entitlements.plist を使用した。

1. Entitlements.plistファイルの作成
plist 形式のファイルを作成する。作成は手動でも可能だが Xcode の新規作成にテンプレート(Code Signing > Entitlements)が用意されているのでそれを使うのが良いだろう。
テンプレートから作成されたファイルの内容は次の通り。
この中の keychain-access-groups(配列)へ所属するグループを列挙しておく。

2. ファイルの設定
Xcode プロジェクトのターゲットの情報を開き「ビルド」タブ内の「コード署名権限」(変数:CODE_SIGN_ENTITLEMENTS)に 1.で作成したファイル名を指定する。
1. で Xcode のテンプレートから Entitlements.plist を作成した場合、この設定は自動的に行われる。

あとはビルドすればいい。

3.4 Keychain Access Group の命名ルール


Keychain Access Group で指定する文字列には次の命名ルールがある。
プロビジョニングファイルで定義される App Identifier と前方一致する
例えば、App Identifier が "GFDZH8DCX.*" の場合、指定可能なグループ名は次のようになる。
GFDZH8DCX.xcatsan.com.KeychainSample
GFDZH8DCX.xcatsan.com.KeychainSample.temporary
GFDZH8DCX.mikeneko
このルールが満たされない場合はアイテム登録時にエラーとなったり、実機へのインストール時にエラーとなる。

例えば、Entitlements.plist ファイルの keychain-access-groups に上記ルールに合わない文字列を指定した場合、実機へインストールするときに "The executable was signed with invalid entitlements." エラーが表示される。
このルールはセキュリティ上の理由によるもので、これによりプロビジョニングファイルが異なる他のアプリケーションが登録したアイテムの読み出しができなくなっている(シミュレータはこのルールが適用されないので他アプリケーションのアイテムが読み出せている)。

3.5 デフォルトグループの決定ルール


kSecAttrAccessGroup を指定せずに SecItemAdd でアイテムを登録した場合、そのアイテムの kSecAttrAccessGroup に設定される値は次の順番で評価される。
  1. kSecAttrAccessGroup
  2. Entitlements.plist/keychain-access-groups
  3. application-identifier
1. SecItemAdd の第一引数で渡す (CFDictionaryRef)attributes に kSecAttrAccessGroup が設定されている場合は、その値が使われる。
2. 1の設定が無い場合は、ビルド時設定 CODE_SIGN_ENTITLEMENTS で指定されたファイル(Entitlement.plist)に記載された keychain-access-groups 配列の1番目の値が使われる。
3. 1と2が共に設定されていない場合は、application-identifer の値が使われる。application-identifier は $(AppIdentifierPrefix)$(CFBundleIdentifier) として定義される。

3.6 アイテム登録時に指定可能なグループ


SecItemAdd で明示的に kSecAttrAccessGroup を指定する場合、指定できるグループ名は Entitlements.plist ファイルの keychain-access-groups 配列内に含まれるか、もしくは application-identifier と一致する必要がある。このルールに従わない文字列を kSecAttrAccessGroup に設定してアイテムを登録(SecItemAdd)しようとする場合、エラーとなる(コード -25243)。 例えばアプリケーションの application-identifier が "GFDZH8DCX.com.xcatsan.keyChainsSample" にもかかわらず "AADZH1DGX.com.apple.Mail" を指定するとエラーになる。

3.7 シミュレータにおけるグループ


シミュレータの場合は実機と異なり Keychain Access Group は常に "test" となる(Entitlements.plist も無視される)。シミュレータ上で動作するアプリケーションはすべて "test"グループに所属するので、他のアプリケーションが登録したアイテムへアクセスすることができる。


3.8 グループ設定の例


前提
App Identifier: GFDZH8PDDD.*
application-identifier: GFDZH8PDDD.com.yourcompany.KeyChainApp
[1] Entitlements.plist なし

[1]-1 登録時のデフォルトグループ
 "GFDZH8PDDD.com.yourcompany.KeyChainApp";

[1]-2 登録時に指定可能なグループ
 "GFDZH8PDDD.com.yourcompany.KeyChainApp";

[1]-3 アクセス可能なグループ例
 "GFDZH8PDDD.com.yourcompany.KeyChainApp";

[1]-4 アクセス不可なグループ例
 "GFDZH8PDDD.";
 "GFDZH8PDDD.com.yourcompany.OtherApp";
 "XJ7GS56DBA.com.apple.MailApp";

(解説)Entitlements.plist を用意しない一般的なパターン。この場合は application-identifier が登録時のデフォルトグループとなる。またアクセスできるのは application-identifier のみ。

[2] Entitlements.plist あり

keychain-access-groups:
 GFDZH8PDDD.private
 GFDZH8PDDD.com.yourcompany.share

[2]-1 登録時のデフォルトグループ
 "GFDZH8PDDD.private";

[2]-2 登録時に指定可能なグループ
 "GFDZH8PDDD.private"
 "GFDZH8PDDD.com.yourcompany.share"
 "GFDZH8PDDD.com.yourcompany.KeyChainApp"

[2]-3 アクセス可能なグループ例
 "GFDZH8PDDD.private";
 "GFDZH8PDDD.com.yourcompany.share";
 "GFDZH8PDDD.com.yourcompany.KeyChainApp";

[2]-4 アクセス不可なグループ例
 "GFDZH8PDDD.other";
 "GFDZH8PDDD.com.yourcompany.OtherApp";
 "XJ7GS56DBA.com.apple.MailApp";

(解説)application-identifier は必ずアクセス可能なグループに入る(ただしデフォルト決定時の優先順位は一番低い)。


4. 開発・運用情報


4.1 API 利用方法


API を利用するにあたっては Xcode のプロジェクトに Security.framework を追加する。
また Security/Scurity.h をインポートしておく。

4.2 実機とシミュレータでの違い


  • 実機の場合は Keychain Service リファレンスの説明の通り他アプリの情報は読み出せない。しかしシミュレータの場合はこの制限がなく読み出しが可能となっている。たとえば kSecClassGenericPassword だけを条件にして SecItemCopyMatching で検索をかけるとそのアプリ以外で登録された情報も取得できる。
  • シミュレータの場合、kSecAttrAccessGroup の設定はできない。常に "test" となる(だから上記の様な挙動となる)。実機の場合はデフォルトで $(AppIdentifierPrefix)$(CFBundleIdentifier) となる。
    (例)GFDZH8DCX.com.xcatsan.keyChainsSample

4.3 アプリを削除した場合の挙動


Keychain Service に登録された情報は、実機・シミュレータ共にアプリを削除しても残る。再びアプリをインストールすると以前の情報を読み出すことが出来る。


5. サンプル


Keychain Services へ ID、パスワードの組を保存、æ›´æ–°、検索、削除するサンプルを作ってみた。
結果はデバッグコンソールに表示される。dump ボタンを押すと現在登録したすべてのアイテム情報を表示する。

ソースコードは理解しやすさを優先させてわざと冗長にしてある。以下にそれぞれの操作の処理を見ていく。

5.1 登録・æ›´æ–°


最初にログインIDをキーにして登録済みかどうかチェックする。登録済みの場合はパスワードのみ SecItemUpdate で更新する。登録がまだの場合は SecItemAdd を使い新規に登録する。
- (IBAction)update:(id)sender
{
 NSMutableDictionary* attributes = nil;
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 NSData* passwordData = [self.password.text dataUsingEncoding:NSUTF8StringEncoding];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)self.loginId.text forKey:(id)kSecAttrAccount];
 [query setObject:SERVICE_NAME forKey:(id)kSecAttrService];
 [query setObject:[IDENTIFIER dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecAttrGeneric];

 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL);
 
 if (err == noErr) {
  // update item
  NSLog(@"SecItemCopyMatching: noErr");

  attributes = [NSMutableDictionary dictionary];
  [attributes setObject:passwordData forKey:(id)kSecValueData];
  [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate];

  err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes);
  if (err == noErr) {
   NSLog(@"SecItemUpdate: noErr");
  } else {
   NSLog(@"SecItemUpdate: error(%d)", err);
  }
  
 } else if (err = errSecItemNotFound) {
  // add new item
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
  
  attributes = [NSMutableDictionary dictionary];
  [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
  [attributes setObject:(id)self.loginId.text forKey:(id)kSecAttrAccount];
  [attributes setObject:passwordData forKey:(id)kSecValueData];

  err = SecItemAdd((CFDictionaryRef)attributes, NULL);
  if (err == noErr) {
   NSLog(@"SecItemAdd: noErr");
  } else {
   NSLog(@"SecItemAdd: error(%d)", err);
  }
  
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
}
配列を渡すこともできるので一度に複数のアイテムを登録することもできる。

5.2 削除


削除したいアイテムの条件を queryに設定し、SecItemDelete へ渡す。
- (IBAction)delete:(id)sender
{
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)self.loginId.text forKey:(id)kSecAttrAccount];
 
 OSStatus err = SecItemDelete((CFDictionaryRef)query);
 
 if (err == noErr) {
  NSLog(@"SecItemDelete: noErr");
 } else {
  NSLog(@"SecItemDelete: error(%d)", err);
 }
}
query に一致するアイテムはすべて(複数)削除される。

5.3 検索(パスワード取得)


query に条件を用意し SecItemCopyMatching へ渡して結果を取得する。
- (IBAction)getPassword:(id)sender
{
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)self.loginId.text forKey:(id)kSecAttrAccount];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
 
 NSData* passwordData = nil;
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query,
            (CFTypeRef*)&passwordData);
 
 if (err == noErr) {
  NSLog(@"SecItemCopyMatching: noErr");
  self.password.text = [[[NSString alloc] initWithData:passwordData
        encoding:NSUTF8StringEncoding] autorelease];
 } else if(err = errSecItemNotFound) {
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }

}

5.4 検索(全アイテム取得)


SecItemCopyMatching の query に kSecMatchLimitAll をセットすると条件に合うすべてのアイテムを取得することができる。条件はクラスのみを指定している。またこの場合の戻り値 result の型は CFArray となる。
- (IBAction)dump:(id)sender
{
 NSMutableDictionary* query = [NSMutableDictionary dictionary];
 
 [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
 [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];
 [query setObject:(id)kSecMatchLimitAll forKey:(id)kSecMatchLimit];
 
 CFArrayRef result = nil;
 OSStatus err = SecItemCopyMatching((CFDictionaryRef)query,(CFTypeRef*)&result);
 
 if (err == noErr) {
  NSLog(@"SecItemCopyMatching: noErr");
  NSLog(@"%@", result);
 } else if(err = errSecItemNotFound) {
  NSLog(@"SecItemCopyMatching: errSecItemNotFound");
 } else {
  NSLog(@"SecItemCopyMatching: error(%d)", err);
 }
 
}
下記は出力例(実機 iOS4.2.1/3GS)。このような dumpメソッドを用意しておくとデバッグの時に役立つ。
KeyChainsSampe[4108:307] (
        {
        acct = hashi;
        agrp = "GFDZH8DCX.com.xcatsan.keyChainsSample";
        pdmn = ak;
        svce = "";
    },
        {
        acct = mikeneko;
        agrp = "GFDZH8DCX.com.xcatsan.keyChainsSample";
        pdmn = ak;
        svce = "";
    },
        {
        acct = hashi;
        agrp = "GFDZH8DCX.com.xcatsan.keyChainsSample";
        cdat = "2011-02-04 05:14:45 +0000";
        desc = "This is a password";
        gena = <70617373 776f7264>;
        mdat = "2011-02-04 05:14:45 +0000";
        pdmn = ak;
        svce = SampleService;
    }
)
キーは4文字に短縮された文字列が使われている。シンボルとの対応は当投稿の付録を参照のこと。


6. ソースコード


GitHub からどうぞ。
KeyChainsSampe at 2011-02-06 from xcatsan/iOS-Sample-Code - GitHub


7. 運用上の注意点


Keychain Services を運用するにあたっては次の点に注意する必要がある。

プロビジョニングファイルを変更しない


「3. アクセス制御」で説明したように、Keychain Services 内のアイテムへのアクセス可否を決める Keychain Access Group はプロビジョニングファイル内の App Identifier に依存する。異なる App Identifier で登録されたアイテムへはアクセスすることができない。バージョンアップ時にプロビジョニングファイルを変えた場合、App Identifier が変わると前のバージョンで登録したアイテムが新しいバージョンでは利用できない、といった問題が起こる。

バックグラウンドアプリケーションは適切な kSecAttrAccessible を設定する


「2. 利用方法 - kSecAttrAccessbile」で説明したように、デフォルトでは iOSデバイスのロックが外れた状態(つまりユーザが操作している時)でしかアイテムへアクセスできないようになっている。ロック中にアイテムへアクセスするようなバックグラウンドアプリケーションの場合、アイテムの kSecAttrAccesible 属性の値を kSecAttrAccessibleAfterFirstUnlock(もしくは kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)に設定しておく必要がある。


8. 参考情報


Keychain Services Programming Guide: Introduction
Keychain Service の概要、サンプルコードなど。

Keychain Services Reference
SecItem系関数リファレンス。

Keychain Services Reference
Mac OS X 用 Keychain Service Reference(SecItem系関数、SecKeychain系関数のリファレンス)。

Simple iPhone Keychain Access - Blog - Use Your Loaf
SecItem系関数の使い方が解説されている。

(旧) Cocoaの日々: Keychain Services 調査 (1) 情報収集
過去に Mac OS X で調査した時の記録など(二十数回続く連載)。

ロックされたiPhoneが6分でハッキング…スマートフォンはもはや電話じゃないことを忘れずに:国内・海外情報から見える『企業のWEB活用法』:ITmedia オルタナティブ・ブログ
jailbreak された iPhone なら専用ツールを使い Keychain Services内のデータが読み出すことができるらしい。最初に触れたように iOS の場合、Keychain Services用のパスワードは(ユーザではなく)iOS 自身が管理する為、例えばそれが格納された場所が特定できればパスワードを解析することは難しくないかもしれない(そんなすぐに見つかる/アクセスできるような配置はしていないとは思うが。でも rootが取られればどうだろう。)。


9. 付録


属性キーの文字列


属性キーのシンボルと実際の文字列との対応表。
kSecAttrAccessiblepdmn
kSecAttrCreationDatecdat
kSecAttrModificationDatemdat
kSecAttrDescriptiondesc
kSecAttrCommenticmt
kSecAttrCreatorcrtr
kSecAttrTypetype
kSecAttrLabellabl
kSecAttrIsInvisibleinvi
kSecAttrIsNegativenega
kSecAttrAccountacct
kSecAttrServicesvce
kSecAttrGenericgena
kSecAttrSecurityDomainsdmn
kSecAttrServersrvr
kSecAttrProtocolptcl
kSecAttrAuthenticationTypeatyp
kSecAttrPortport
kSecAttrPathpath
kSecAttrSubjectsubj
kSecAttrIssuerissr
kSecAttrSerialNumberslnr
kSecAttrSubjectKeyIDskid
kSecAttrPublicKeyHashpkhh
kSecAttrCertificateTypectyp
kSecAttrCertificateEncodingcenc
kSecAttrKeyClasskcls
kSecAttrApplicationLabelklbl
kSecAttrIsPermanentperm
kSecAttrApplicationTagatag
kSecAttrKeyTypetype
kSecAttrKeySizeInBitsbsiz
kSecAttrEffectiveKeySizeesiz
kSecAttrCanEncryptencr
kSecAttrCanDecryptdecr
kSecAttrCanDerivedrve
kSecAttrCanSignsign
kSecAttrCanVerifyvrfy
kSecAttrCanWrapwrap
kSecAttrCanUnwrapunwp
kSecAttrAccessGroupagrp

kSecAttrAccessible の文字列


kSecAttrAccessibleAfterFirstUnlockck
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnlycku
kSecAttrAccessibleAlwaysdk
kSecAttrAccessibleAlwaysThisDeviceOnlydku
kSecAttrAccessibleWhenUnlockedak
kSecAttrAccessibleWhenUnlockedThisDeviceOnlyaku


- - - -
ここまで読んだ方の中で Keychain Services がアプリ間の情報共有の用途に使えることに気がついた人がいるかもしれない。答えは半分イエスで半分ノー(制限がある)。次回その方法について解説する。

[iOS] 起動に時間がかかりすぎるとクラッシュする(原因と対策など)

2011年2月5日土曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

起動時間が長いとクラッシュ


iOS アプリは起動に時間がかかりすぎるとクラッシュする。具体的にはアプリで -[UIApplication application:didFinishLaunchingWithOptions:] での処理が規定された時間を越えた場合に iOS によって強制終了させられてしまう。これが原因で落ちた場合はクラッシュレポートの execution code が 0x8badf00d ("ate bad food") が出る。
(例)Incident Identifier: A3345F94-896E-4D74-AC14-282B3A8996C7
CrashReporter Key:   5e0a8626193824c21a4b9fef635fa1542c92d1c2
   :
Exception Type:  00000020
Exception Codes: 0x8badf00d
Highlighted Thread:  0
 
Application Specific Information:
LazyStartSample failed to launch in time

Elapsed total CPU time (seconds): 1.530 (user 0.590, system 0.940), 8% CPU 
Elapsed application CPU time (seconds): 0.270, 1% CPU
   :
Technical Note TN2151: Understanding and Analyzing iPhone OS Application Crash Reports によれば
Watchdog timeout: distinguished by exception code 0x8badf00d. Timeouts occur when an application takes too long to launch, terminate, or respond to system events.
とある。Watchdog(監視プロセス)がアプリの起動時間を監視していて一定時間を超過するとそのアプリを強制終了する。限界時間はドキュメントで見つけられなかったが実測値で 20秒程度だった(iOS4.2.1/3GS)。


起動時間制限の例外


Technical Q&A QA1592: Application does not crash when launched from debugger but crashes when launched by user. によれば Xcode を経由しての実機デバッグ実行時はこの制限が外れるとのこと。これはアプリ本体の実行時間とは別にデバッガの初期化処理などで時間がかかる為。この為開発中には問題が出なかったに、リリース後に問題が表面化するということもありうる。


サンプル


-[UIApplication application:didFinishLaunchingWithOptions:] で単純に 25秒間 sleep するコードを書いてみた。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    
    
    [self.window addSubview:viewController.view];
    [self.window makeKeyAndVisible];

 NSLog(@"%s|%@", __PRETTY_FUNCTION__, @"sleeping...");
 [NSThread sleepForTimeInterval:25.0];
 NSLog(@"%s|%@", __PRETTY_FUNCTION__, @"finished");
 
    return YES;
}

実機で実行させしばらくした後に意図通りアプリが落ちる。ログは次の通り。
Incident Identifier: A3345F94-896E-4D74-AC14-282B3A8996C7
CrashReporter Key:   5e0a8626193824c21a4b9fef635fa1542c92d1c2
Hardware Model:      iPhone2,1
Process:         LazyStartSample [315]
Path:            /var/mobile/Applications/88AB96AC-0253-418B-8AE3-....
Identifier:      LazyStartSample
Version:         ??? (???)
Code Type:       ARM (Native)
Parent Process:  launchd [1]

Date/Time:       2011-02-11 16:13:03.788 +0900
OS Version:      iPhone OS 4.2.1 (8C148a)
Report Version:  104

Exception Type:  00000020
Exception Codes: 0x8badf00d
Highlighted Thread:  0

Application Specific Information:
LazyStartSample failed to launch in time

Elapsed total CPU time (seconds): 1.530 (user 0.590, system 0.940), 8% CPU 
Elapsed application CPU time (seconds): 0.270, 1% CPU

Thread 0:
0   libSystem.B.dylib              0x311de9f0 0x31165000 + 498160
1   libSystem.B.dylib              0x31167e28 0x31165000 + 11816
  :
sleep() なので(当然ながら)CPU 時間はほとんど使っていないが、起動時間が長いと強制終了させられる。


対策


アプリ起動時間を減らすしかない。でも時間がかかる処理があるとしたらどうするか。例えば Core Data のマイグレーションが行われる場合、処理件数が多いと数十秒かかるケースがある。この時間は Core Data を使っている限りは避けられない。とすると、処理時間がかかる処理の実行タイミングを遅らせるしかない。サンプルでは、-[UIApplication application:didFinishLaunchingWithOptions:] に sleep(25) を入れていた。試しに -applicationDidBecomeActive:に入れみたが駄目。では最初に表示されるビューコントローラのメソッドへ移したらどうか。
× viewDidLoad
× viewWillAppear:
× viewDidAppear:
結果は×。最初のビューの初期化タイミングはアプリケーション起動時間に含まれてしまう。コールスタックを見ると理由がわかる(NSLog(@"%@", [NSThread callStackSymbols]);)。
0   0x00002609 -[LazyStartSampleViewController viewDidAppear:] + 44
 1   0x338e52e7 -[UIViewController viewDidMoveToWindow:shouldAppearOrDisappear:] + 446
 2   0x338b86ab -[UIView(Internal) _didMoveFromWindow:toWindow:] + 506
 3   0x338b8433 -[UIView(Hierarchy) _postMovedFromSuperview:] + 106
 4   0x338a382f -[UIView(Internal) _addSubview:positioned:relativeTo:] + 678
 5   0x338a357f -[UIView(Hierarchy) addSubview:] + 22
 6   0x00002307 -[LazyStartSampleAppDelegate application:didFinishLaunchingWithOptions:] + 82
  :
application:didFinishLaunchingWithOptions: の処理内だからまだアプリケーション起動が完了していないということになる。
試しにビューにボタンを付けて、起動後にユーザが押した場合に sleep(25)すると問題なく処理できた。
どうするか。どうにかして application:didFinishLaunchingWithOptions: をやり過ごし、次のイベントループに入ったところで時間のかかる処理を回すことができれば起動時間制限にかからず行けるかもしれない。タイマーをしかけたり遅延実行する方法も考えられるが、それとは別の方法としてメインのビューを表示した後に一旦別のビューを表示してそこで処理させたらどうだろうか。どの道時間がかかる処理なので処理中であることをユーザへ見せる必要もあるので不自然ではない。

試しに SubViewController を新たに作り、メインのビューの -viewDidAppear: の中からこのビューをモーダル表示させてみた。
- (void)viewDidAppear:(BOOL)animated
{
 SubViewController* viewController = [[SubViewController alloc] init];
 [self presentModalViewController:viewController
       animated:YES];
 [viewController release];
}
そしてサブビューの -viewDidAppear: 内で sleep(25)をかけてみる。
- (void)viewDidAppear:(BOOL)animated
{
  NSLog(@"%s|%@", __PRETTY_FUNCTION__, @"sleeping...");
  [NSThread sleepForTimeInterval:25.0];
  NSLog(@"%s|%@", __PRETTY_FUNCTION__, @"finished");
}
さて実行する。起動後一瞬だけメインビューが表示された後、下からサブビューがせり上がってきてモーダル表示される。
そして待つこと25秒。
2011-02-04 23:08:50.517 LazyStartSample[69102:207] -[SubViewController viewDidAppear:]|sleeping...
2011-02-04 23:09:15.869 LazyStartSample[69102:207] -[SubViewController viewDidAppear:]|finished
出た。無事に処理し終えた。

ちなみにサブビューの viewDidAppear: におけるコールスタックは次の通り。
0   0x0000275d -[SubViewController viewDidAppear:] + 44
 1   0x33972301 -[UIWindowController transitionViewDidComplete:fromView:toView:] + 860
 2   0x3391af83 -[UITransitionView notifyDidCompleteTransition:] + 106
 3   0x3391ae1b -[UITransitionView _didCompleteTransition:] + 798
 4   0x33971f9b -[UITransitionView _transitionDidStop:finished:] + 34
 5   0x338b4337 -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 190
 6   0x338c0c15 -[UIViewAnimationState animationDidStop:finished:] + 40
 7   0x30a89ea9 _ZL23run_animation_callbacksdPv + 292
 8   0x30a89d4b _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv + 122
 9   0x314870a3 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 14
 10  0x31486b5b __CFRunLoopDoTimer + 850
 11  0x314581b5 __CFRunLoopRun + 1088
application:didFinishLaunchingWithOptions: とは無関係にランループのイベントから処理が始まっている。つまりアプリ起動時間にはカウントされない。

この方法を使う場合の流れのイメージは次のとおり。
1. メインビュー viewDidAppear: で時間のかかる処理が必要か判断する。
 (例えば Core Data のマイグレーション)
2. 処理が必要な場合、サブビューを呼び出しそこで時間のかかる処理を行う。
 この時、ユーザに処理中であることを表示する(ウェイトカーソルなど。必要なら
 キャンセルボタン)。終了後は自動もしくはユーザの確認の上、メインビューへ戻る。
3. 処理が不要な場合はそのままメインビューで処理を続行する。

ソースコード


GitHub からどうぞ。
LazyStartSample at 2011-02-04 from xcatsan/iOS-Sample-Code - GitHub


参考情報


"ate bad food"
0x8BADF00D ("ate bad food") is used by Apple in iOS crash reports, when an application takes too long to launch,

Cocoaの日々: +[NSThread callStackSymbols]でコールスタックのダンプ

- - - -
現在アプリバージョンアップに伴う Core Data マイグレーションでまさにこの問題に直面中。上記アプローチでまずは対応する予定だがうまく行ったらその顛末を後ほどブログで紹介する。

switch文でローカル変数を宣言する

2011年2月4日金曜日 | Published in | 6 コメント

このエントリーをはてなブックマークに追加

私はローカル変数をそれを使うコードの直前で定義する派(?)なのだが、switch文のケース内で定義ができなくていつも残念に思っていた。
今日ふと気がついてブロック { } で囲ってみた。

するとコンパイルが通った。
おお、これはいい。

#この既述が気持ち悪い人もいるかもね。

[Mac] Event Monitor 〜 Cocoaにおけるホットキー実装に使えるか?

2011年2月3日木曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

リファレンスを眺めていたらたまたま Event Monitor という APIを見つけた。10.6 から導入された仕組みで他のアプリのイベントをキャプチャできるらしい。


Event Monitor


Cocoa Event-Handling Guide: Monitoring Events

Mac OS X v10.6 から NSEvent に Event Monitor が導入された。Local Monitor と Global Monitor の2種類があり、前者はアプリ内のイベント、後者は他のアプリのイベントをキャプチャすることができる。API はこんな感じで Blocks で処理を記述するようになっている。
NSEvent Class Reference
+ (id)addGlobalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(void (^)(NSEvent*))block;
+ (id)addLocalMonitorForEventsMatchingMask:(NSEventMask)mask handler:(NSEvent* (^)(NSEvent*))block;
受け取れるイベントはほぼすべて
NSFlagsChanged
NSLeftMouseDragged
NSRightMouseDragged
NSOtherMouseDragged
NSLeftMouseUp
NSRightMouseUp
NSOtherMouseUp
NSLeftMouseDown
NSRightMouseDown
NSOtherMouseDown
NSMouseMoved
NSFlagsChanged
NSScrollWheel
NSTabletPoint
NSTabletProximity
NSKeyDown (Key repeats are determined by sending the event an isARepeat message.)
使い方は簡単でアプリ起動時などに上記クラスメソッドを呼び出してイベントハンドラを登録するだけ。こんな感じ。
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
 // Insert code here to initialize your application 
 
 [NSEvent addGlobalMonitorForEventsMatchingMask:NSKeyDownMask
             handler:^(NSEvent* event) {
              NSLog(@"GlobalMonitor: %@", event);
             }];
 
 [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask
            handler:^(NSEvent* event) {
             NSLog(@"LocalMonitor: %@", event);
             return event;
            }];
}


制約


便利な Event Monitor だが実は標準では使えない。Global Monitor を使ってイベントを受け取るにはユニバーサルアクセスの「補助装置にアクセスできるようにする」にチェックを入れる必要がある。
これが無いと Global Monitor からのイベントは受け取れない。Local Monitor はこの設定の有無に関わらわず受け取ることができる。


考察


とても強力な API だが、ユニバーサルアクセスの設定が必要なのと、何よりも悪用される可能性が高いことから使い道はかなり限られる。別の言い方をすると非常に簡単にキーロガーやイベントロガーの作成が可能なので、もし「補助装置にアクセスができるようにする」にチェックが入っていると簡単にそういったソフトの標的になりうる。この為、Global Event Monitor を使いこの設定を前提としたアプリケーションの配布は行うべきではないだろう(ユーザが一旦設定してしまうと悪意を持った他のアプリに容易にイベントを盗聴されてしまう可能性が高まる)。便利なんだが....残念。


ソースコード


GitHub からどうぞ。
EventMonitorSample at 2011-02-03 from xcatsan/MacOSX-Sample-Code - GitHub
※「補助装置にアクセスできるようにする」を有効にする場合は考察で述べた危険性があることを十分に理解した上で行って下さい(自己責任です)。


[参考情報] Carbon API を使ったホットキーの実装


Mac OS X でホットキーを実現しようとすると Carbon API を使う必要がある。こんな感じ。
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
  :
 EventTypeSpec eventTypeSpecList[] ={
  { kEventClassKeyboard, kEventHotKeyPressed }
 };

 InstallApplicationEventHandler(&hotKeyHandler, GetEventTypeCount(eventTypeSpecList),
    eventTypeSpecList, self, NULL);
 EventHotKeyID hotKeyID;
 hotKeyID.id = 0;
 hotKeyID.signature = 'htky';
 UInt32 hotKeyCode = 49;
 UInt32 hotKeyModifier = cmdKey + optionKey;
 
 OSStatus status = RegisterEventHotKey(hotKeyCode, hotKeyModifier, hotKeyID,
    GetApplicationEventTarget(), 0, &_hotKeyRef);
  :
}
[参考情報] (旧) Cocoaの日々: ホットキー

利用方法に特に問題はないが Carbon API なのでいずれは利用できなくなるという根本的な問題がある。


参考情報


Global hotkeys in Cocoa on Snow Leopard

Shortcut Recorder

[Mac] トラックパッド 〜 deviceSize でトラックパッド実物の大きさがわかる?

2011年2月2日水曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

[前回] Cocoaの日々: [Mac] トラックパッド 〜 2本指によるドラッグを扱う

トラックパッド実物の大きさを計算してみる


MacBook Pro 12'(1280x800) の場合、-[NSTouch normalizedPosition] の戻り値は次のとおり。
{297.638, 215.433}
72ppiで逆算すると
横幅:297[pixel] / 72[ppi] = 4.125[inch] = 10.5[cm]
縦幅:215[pixel] /72[ppi] = 2.986[inch] = 7.6[cm]
実物のトラックパッドの大きさを測ってみたところほぼ同じ。おー。


deviceの値


-[NSTouch device] というメソッドもある。こちらの戻り値をログへ表示すると下記のようになった。
<NSObject: 0x100551f30>
リファレンスによると複数のタッチデバイスを使った場合の識別に使えるようなことが書いてあった。それ自身詳しい情報が得られるわけではなく単純な ID用途のオブジェクトのようだ。


参考情報


NSTouch Class Reference

[Mac] トラックパッド 〜 2本指によるドラッグを扱う

2011年2月1日火曜日 | Published in | 0 コメント

このエントリーをはてなブックマークに追加

トラックパッドで2本指のドラッグを扱ってみた。

サンプル


サンプルを起動するとウィンドウが1つ立ち上がる。

この中に表示されている画像を2本指でドラッグすると画像がそれに追随して移動する。


実装


画像を表示し、トラックパッドのイベントを処理するカスタムビューを用意する。
@interface GestureView : NSView {

 NSImage* image_;
 BOOL isTracking_;

 NSTouch* previousTouch_;
}

@property (nonatomic, retain) NSTouch* previousTouch;
@end
タッチイベントは NSResponder の関連メソッドをオーバライドして処理する。まず touchesBeganWithEvent: において2本指のタッチを検出し、その時の片方の指の NSTouchインスタンスを previousTouchへ取っておく。
- (void)touchesBeganWithEvent:(NSEvent *)event {

    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self];
    
    if (touches.count == 2) {
  
        self.previousTouch = [touches anyObject];
    }

}
続いて移動時に呼び出される touchesMovedWithEvent: で2本指の場合にビューの移動処理を行う。
- (void)touchesMovedWithEvent:(NSEvent *)event {
    
    NSSet *touches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self];
    
    if (touches.count == 2 && self.previousTouch) {
  
  NSTouch *currentTouch = nil;
  for (currentTouch in [touches allObjects]) {
   if ([currentTouch.identity isEqual:self.previousTouch.identity]) {
    break;
   }
  }

  NSSize deviceSize = self.previousTouch.deviceSize;
  CGFloat dx = (currentTouch.normalizedPosition.x -
       self.previousTouch.normalizedPosition.x) * deviceSize.width * 2;
  CGFloat dy = (currentTouch.normalizedPosition.y -
       self.previousTouch.normalizedPosition.y) * deviceSize.height * 2;
  
        if (!isTracking_) {

   if (fabs(dx) > kThresholdOfDoubleTouches || fabs(dy) > kThresholdOfDoubleTouches) {
                isTracking_ = YES;
            }

        } else {
   NSRect frame = self.frame;
   frame.origin.x += dx;
   frame.origin.y += dy;
   self.frame = frame;
        }
  self.previousTouch = currentTouch;
    }
}
NSTouch の identify プロパティを使って触れられた指それぞれについて touchesイベントを通じて識別が行える。ここでは touchesBeganWithEvent: の時に取っておいた previousTouch と比較して同じ指の NSTouch を探し出し、それを元に移動量を計算している。


ソースコード


GitHub からどうぞ。
GestureSample at 2011-02-01 from xcatsan/MacOSX-Sample-Code - GitHub


2本指タッチの扱い


今回は簡単な方法を採用して移動量の計算には1本分の指しか使っていない。この為、ドラッグ中に2本指の間が離れたり近づいたりしても支障なくドラッグ操作が行える。通常2本指でドラッグする場合は2本の指を揃えて操作するのでこれで問題はないと思われる。簡単なしくみなのでソースコード上の数値(ï¼’)を書き換えれば3本指、4本指のドラッグも簡単に処理できる。なお拡大縮小のピンチ操作を扱おうとするとドラッグ処理を区別する必要がある。今回のサンプルに単純に magnifyWithEvent: を実装するとピンチ操作時には magnifyWithEvent: と touchesMovedWithEvent: が交互に呼び出される。もし今回のサンプルで拡大縮小を実装する場合はピンチ時に画像が移動するのはまずいので両方の操作をきちんと区別する必要がある。前回みたサンプル LightTable では指の間の距離も考慮していた。

[参考] Cocoaの日々: [Mac] トラックパッド 〜 タッチイベントサンプル LightTable のソースを読む


デバイスごとのスケーリング


-[NSTouch normalizedPosition] で取得できる値は 0〜1.0 の間に収まる様に正規化されたものとなっている。この為(移動などで)利用する場合は調整する必要がある。NSTouchには使用しているタッチデバイスのサイズが deviceSizeとして取得できるのでこれを係数として利用することができる(deviceSizeの値は 72ppi換算のピクセル値)。サンプルでは移動量の計算にこの値を使っている。なおコード上ではさらに2倍の係数をかけている。これは画面上で実際にドラッグして調整した値。このあたりシビアなドラッグ操作が要求されるアプリではユーザがカスタマイズできるようにした方が良いと思われる。


参考情報


NSTouch Class Reference

.

人気の投稿(過去 30日間)