[iOS] データ保護(Data Protection)

2011年4月22日金曜日 | Published in | 3 コメント

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

データ保護


iOS 4 以降、データ保護機能が提供されるようになった。特徴は次の通り。

  • データ保護機能を有効にするにはパスコードロックを有効にする必要がある
  • データ保護の適用はファイル単位となる
  • アプリケーションが明示的にファイルにデータ保護属性を付加することで有効になる
  • データ保護属性のついたファイルには、デバイスロック中は保護されていてアクセスができない
  • バックグラウンドで動作するアプリであっても、デバイスロック時にはデータ保護されたファイルへアクセスできない
  • データ保護属性のついたファイルは、デバイスロック中に iTunesなどのツールから持ち出すことができない
  • データ保護属性のついたファイルは暗号化される
  • 対象機種:iPhone 4, iPhone 3GS, iPod touch (3rd generation or later), and all iPad models
[参考情報] Limitations of Data Protection in iOS 4 | Anthony Vance

※上記は若干自信が無いところもあるので違っていたら是非教えて下さい。


データ保護機能を有効にするにはパスコードロックが有効になっている必要があり、有効の場合はその旨メッセージが表示される。

※iOS 3から 4 へアップデートした場合にはこのメッセージが出ない。この場合「復元」操作が必要。
iOS 4:データ保護について


データ保護 API


データ保護向けの APIがいくつか用意されている。

NSFileManager

ファイル属性に NSFileProtectionKey が追加された。値に NSFileProtectionComplete を指定するとデータ保護が有効になる。
NSDictionary* attributes =
    [NSDictionary dictionaryWithObject:NSFileProtectionComplete forKey:NSFileProtectionKey];
NSError* error = nil;
[fileManager setAttributes:attributes ofItemAtPath:filePath error:&error];
NSFilManager Reference - File Protection Values

なおファイルコピー時に NSFileProtectionComplete の設定はコピーされない。コピー先のファイルは NSFileProtectionNone となる。

NSData

ファイル書き出し時に NSDataWritingFileProtectionComplete オプションを指定するとデータ保護の適用を指定することができる。
NSError* error = nil;
[data writeToFile:filePath
 options:NSDataWritingFileProtectionComplete error:&error];
NSData Class Reference - NSDataWritingOptions

UIApplicationDelegate

データ保護が有効になる直前、無効になる直前に呼ばれるデリゲートメソッドが用意されている。
- (void)applicationProtectedDataDidBecomeAvailable:(UIApplication *)application
- (void)applicationProtectedDataWillBecomeUnavailable:(UIApplication *)application
UIApplicationDelegate Protocol Reference

また UIApplication には保護データへアクセス可能かどうかを知ることができるプロパティ protectedDataAvailable が用意されている。
UIApplication Class Reference


サンプル


(1)データ保護なし (2)データ保護あり(NSFileManagerで属性設定) (3)データ保護あり(NSDataで書き出し)の3種類のファイルを作成してデータの持ち出しと属性を調べてみた。コードはこんな感じ。

// definitions
    NSString* basePath = [NSSearchPathForDirectoriesInDomains(
        NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSFileManager* fileManager = [NSFileManager defaultManager];

    NSString* src1 = [[NSBundle mainBundle] pathForResource:@"sample" ofType:@"jpg"];
    NSString* src2 = [[NSBundle mainBundle] pathForResource:@"sample_encrypted" ofType:@"jpg"];

    NSString* dst1 = [basePath stringByAppendingPathComponent:@"sample.jpg"];
    NSString* dst2 = [basePath stringByAppendingPathComponent:@"sample_encrypted.jpg"];
    NSString* dst3 = [basePath stringByAppendingPathComponent:@"sample_encrypted2.jpg"];
    
    NSLog(@"dst1: %@", dst1);
    NSLog(@"dst2: %@", dst2);
    NSLog(@"dst3: %@", dst3);
    
    NSError* error = nil;
    
    // copy files
    if (![fileManager copyItemAtPath:src1 toPath:dst1 error:&error]) {
        NSLog(@"%@", error);        
    }
    if (![fileManager copyItemAtPath:src2 toPath:dst2 error:&error]) {
        NSLog(@"%@", error);        
    }

    // set attributes
    NSDictionary* attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
                                                           forKey:NSFileProtectionKey];
    if (![fileManager setAttributes:attributes
                       ofItemAtPath:dst2
                              error:&error]) {
        NSLog(@"%@", error);
    }
    
    // create dst3
    NSData* data = [NSData dataWithContentsOfFile:src1];
    if (![data writeToFile:dst3
                   options:NSDataWritingFileProtectionComplete
                     error:&error]) {
        NSLog(@"%@", error);
    }
あらかじめ JPEG画像を用意しておき、これを Documentディレクトリへコピーしたり(1)(2)、書きだしたり(3)している。


iTunes でアクセス


このアプリを実行した後、iTunes へ繋ぎデータを持ち出しを試みてみた。

予想通りデータ保護のかかっている(2)(3)のファイルは持ち出し時にエラーが出た。

データ保護はパスコードロックがかかっている時のみ有効。


試しにそれぞれのファイル属性を出してみた。
2011-04-22 10:26:20.894 FileProtectionSample[2272:707] dst1: {
    NSFileCreationDate = "2011-04-20 07:18:08 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-20 07:18:08 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionNone;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 77317;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
2011-04-22 10:26:20.912 FileProtectionSample[2272:707] dst2: {
    NSFileCreationDate = "2011-04-20 07:18:08 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-20 07:18:08 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionComplete;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 77318;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
2011-04-22 10:26:20.929 FileProtectionSample[2272:707] dst3: {
    NSFileCreationDate = "2011-04-20 07:53:00 +0000";
    NSFileExtensionHidden = 0;
    NSFileGroupOwnerAccountID = 501;
    NSFileGroupOwnerAccountName = mobile;
    NSFileModificationDate = "2011-04-22 01:26:20 +0000";
    NSFileOwnerAccountID = 501;
    NSFileOwnerAccountName = mobile;
    NSFilePosixPermissions = 420;
    NSFileProtectionKey = NSFileProtectionComplete;
    NSFileReferenceCount = 1;
    NSFileSize = 3896;
    NSFileSystemFileNumber = 13804;
    NSFileSystemNumber = 234881027;
    NSFileType = NSFileTypeRegular;
}
NSFileProtectionKey が設定されているのがわかる。またファイルサイズに違いは無い。APIを使ったデータ保護設定の有無は単純に属性設定の違いだけのようだ。

なおシミュレータでは "NSFileProtectionKey" の値が含まれない。確認できるのは実機だけのようだ。


Xcode でアクセス


Xcode のオーガナイザでアプリのフォルダ一式をダウンロードしてみる。
"Download"ボタンでPCへ保存しようとすると途中でエラーが出る。


ダウンロード


サンプルのソースコードは GitHub からどうぞ。
FileProtectionSample at 2011-04-21 from xcatsan/iOS-Sample-Code - GitHub


参考情報



「11-3 データ保護」が詳しく参考になった。


Core Data and Enterprise iPhone Applications – Protecting Your Data << Nick Harris
今回のデータ保護機能を使って CoreData のデータ(SQLite)を暗号化する方法が紹介されている。


Limitations of Data Protection in iOS 4 | Anthony Vance
データ保護についての特徴と制約がよくまとめられている。データ保護関連の情報は少ないので貴重な解説。


iOS 4: Data protection, hardware encryption and other insight
データ保護、ハードウェア暗号化の話題など。

Working with Protected Files
データ保護機能の使い方。setAttributes: を使う場合はデータを書きだす前に NSFileProtectionComplete を設定することが推奨されている。


iTunesを使ったファイル共有機能を使う方法 - 強火で進め
iTunes で iOSデバイス内のフォルダへアクセスする方法について。Info.plist で "Application supports iTunes file sharing" を有効にすると iTunes の Appタブでアプリケーションの Documents フォルダへアクセスすることができるようになる。今回のサンプルはこれが有効になっている。


iOS 4:データ保護について
データ保護を有効にする方法。iOS 3からのバージョンアップの場合はデバイスの復元が必要。

[iOS] バックグラウンド実行見本(Task Completion)

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

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

(2011-12-01 追記あり)UIApplicationDelegateの呼び出しが iOS5 から変わった件。

Task Completion を使った iOS4 でのバックグラウンド実行サンプルを作ってみた。

サンプル


実行するとキューにたまった 30個のデータが順番に処理されてテーブルから消えていく。

処理は GCD を使い別スレッドで実行される。右上の[+]ボタンを押すとキューへデータが追加されていく。途中でホームボタンを押してアプリを切り替えても Task Completion によって処理は停止すること無く実行され続ける。わかりやすいようにアプリのアイコンバッヂに残タスク数を表示してみた。
バッジの数字は時間と共にカウントダウンされていくので処理が行われていることが確認できる。


Task Completion とは?


Task Completion は、iOS4 から導入されたマルチタスキングの機能の一つで、これを利用すると最大10分間を上限にバックグラウンドで処理を実行できる。利用するには -[UIApplication beginBackgroundTaskWithExpirationHandler:] を使う。このメソッドは iOS に対してバックグラウンド時の処理続行を依頼するものなので通常はアプリケーションがバックグラウンド状態になるタイミング(applicationDidEnterBackground:)などで呼び出す。公式リファレンスでは次のようなコードが紹介されている。
iOS Application Programming Guide: Executing Code in the Background より

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    UIApplication*    app = [UIApplication sharedApplication];
 
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
 
    // Start the long-running task and return immediately.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
 
        // Do the work associated with the task.
 
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}
beginBackgroundTaskWithExiprationHandler: を呼び出すと、それ以降別のアプリを使用している間も処理を続行させることができる。このメソッドの引数 blocks には、処理が10分過ぎても終わらない時に実行する処理を書いておく。endBackgroundTask: はバックグラウンド処理が終わった時に呼び出す。この引数は beginBackgroundTaskWithExiprationHandler: の戻り値を渡す。


ソースコード解説


サンプルでの Task Completion の利用方法を見ていく。今回は簡易的なキュー(FIFOバッファ)を用意して、そこに入っているデータを単純に取り出すだけの処理を行うスレッドを実行させた。キューの定義はこんな感じ。
@interface Queue : NSObject {

}
@property (retain) NSMutableArray* queue;

- (void)putObject:(id)object;
- (id)getObject;
- (void)removeObject;
- (NSUInteger)count;
- (NSArray*)list;
@end
次にアプリケーション初期化処理。
- (BOOL)application:(UIApplication *)application
 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // [1] creating sample data and put into the queue
    Queue* queue = [[Queue alloc] init];
    for (int i=0; i < 60; i++) {
        NSString* str = [NSString stringWithFormat:@"DATA-%02d", i];
        [queue putObject:str];
    }
    self.rootViewController.queue = queue;
    [queue release];

    // [2] init window    
    self.window.rootViewController = self.navigationController;
    [self.window makeKeyAndVisible];
    
    // [3] start thread
    dispatch_queue_t gcd_queue =
       dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gcd_queue, ^{
        
        UIApplication* app = [UIApplication sharedApplication];
        app.applicationIconBadgeNumber = [queue count];
        for(;;) {
            if ([queue count] > 0) {
                id object = [queue getObject];
                NSLog(@"processing: %@", object);
                [NSThread sleepForTimeInterval:1.0];    // dummy wait
                NSLog(@"done: %@", object);
                [queue removeObject];
                app.applicationIconBadgeNumber = [queue count];
                
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self.rootViewController.tableView reloadData];
                });
                
                if ([queue count] == 0 && backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        NSLog(@"finished!");
                        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                            [app endBackgroundTask:backgroundTaskIdentifer];
                            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
                        }
                    });
                }
            } else {
                [NSThread sleepForTimeInterval:1.0];
            }
        }
        
    });
    
    return YES;
}
前半ではキューへデータを入れたり[1]、ウィンドウの初期化[2]を行っている。後半の [3]で GCDを使いスレッドを一つ作成し、そこで定期的にキュー内のデータを処理させている。このスレッド内ではキューから1づつデータを取り出し処理(単純に1秒スリープ)を繰り返し行う。すべての処理が終わったときに Task Completion が有効( != UIBackgroundTaskInvalid)なら endBackgroundTask: を読んでバックグラウンド処理を終了させている。キューが空になった時には1秒間スリープし、その後キューにデータがあれば処理を実行し、無ければ再びスリープする動作を繰り返す。

Task Completion を有効にする処理は applicationWillResignActive: に書いている。
- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    UIApplication* app = [UIApplication sharedApplication];
    
    NSAssert(backgroundTaskIdentifer == UIBackgroundTaskInvalid, nil);
    
    backgroundTaskIdentifer = [app beginBackgroundTaskWithExpirationHandler:^{
        
        NSLog(@"expired!");
        dispatch_async(dispatch_get_main_queue(), ^{
            if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
                [app endBackgroundTask:backgroundTaskIdentifer];
                backgroundTaskIdentifer = UIBackgroundTaskInvalid;
            }
        });
    }];
    
}
なお Task Completion を有効にした後に別アプリへ切り替え、さらにその後再びこのアプリへ戻ってきた時にまだ Task Completion が有効だった場合には endBackgroundTask: を呼び出してやる必要がある。これを applicationDidBecomeActive: に書いておく。
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    NSLog(@"%s", __PRETTY_FUNCTION__);
    
    UIApplication* app = [UIApplication sharedApplication];
    dispatch_async(dispatch_get_main_queue(), ^{
        if (backgroundTaskIdentifer != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:backgroundTaskIdentifer];
            backgroundTaskIdentifer = UIBackgroundTaskInvalid;
        }
    });
}

Task Completion の制御を
applicationDidEnterBackground:
applicationWillEnterForeground:
の組ではなく
applicationWillResignActive:
applicationDidBecomeActive:
の組を使うのは、スリープボタンを押した時は前者の組み合わせは呼ばれない為。後者であればスリープ => 復帰の時に呼び出されるので今回の目的に適している。

(2011-12-01 追記)iOS5からスリープボタンを押した時でも前者の組み合わせが呼び出されるとのこと。(@hkato193 さん Thanksです)

参考:Cocoaの日々: UIApplicationDelegate のマルチタスキング関連メソッド調査

ちなみに Task Completion を使うとスリープ中もバックグラウンド処理は走り続ける(上限10分)。


ダウンロード


サンプルのソースは GitHub からどうぞ。

BackgroundQueueSample/BackgroundQueueSample at 2011-04-08b from xcatsan/iOS-Sample-Code - GitHub


参考情報



Task Completion の情報は @hkato193さんが書いている第2章の「マルチタスキング」が詳しい。入手可能な日本語の情報ではこれが一番いい。
同様に GCD や Blocks の解説もこの本の第5章「マルチスレッド」(@splhackさん執筆)が詳しくておすすめ。

iOS Application Programming Guide: Executing Code in the Background
Apple提供情報。

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