CLOSE

SQLCipherとRoom/CoreDataとの親和性について(全2記事)

2021.12.13

Brand Topics

PR

LINEはiOSのデータベース暗号化をどのように実現しているか SQLCipherとCoreDataを組み合わせた暗号化の仕組み

提供:LINE株式会社

2021年11月10日と11日の2日間、LINE株式会社が主催するエンジニア向け技術カンファレンス「LINE DEVELOPER DAY 2021」がオンラインで開催されました。そこで高梨雄大氏とBruce Evans氏が、スマートフォン内に保存しているデータベースの暗号化に対するLINEの取り組みについて共有しました。後半はiOS側についてBruce Evans氏から。前半はこちら。

CoreDataのセットアップ

Bruce Evans氏:では、ここからはiOSの話になります。LINEでは昔からCoreDataを利用して、大量のコードがCoreDataに依存しています。CoreDataは、AndroidのRoomのように、ハイレベルなパーシステンスフレームワークですがApple専用のものです。

これから、このCoreDataのセットアップ、データベース作成、ID変更、検索、保存、そしてデータの読み込みを紹介します。

まずは、CoreDataは暗号化のサポートがないため、最初から暗号化の仕組みを加える必要があります。今表示している画像は、CoreDataの基本的なかたちです。NSManagedObjectContextは、プログラマーがふだん使っている部分です。オブジェクトやエンティティがこのレイヤーなので、CoreDataのメイン機能と考えてもいいと思います。

その下はNSPersistentStoreCoordinatorです。NSPersistentStoreCoordinatorは、Storeを複数管理するレイヤーだけです。そしてNSPersistentStoreは、ファイルストアのレベルで、最もraw SQLiteと近いです。

では、NSPersistentStoreをサブクラスしましょう。でもその前に、サブクラスしたあとにどうやって使うかをまず見てみましょう。

このコードは、カスタムのMySecureStoreをCoreDataに接続します。はじめに、MySecureStoreを登録する必要があります。これはUICollectionViewと似ていて、MySecureStoreのクラスをStringIDに紐づけます。

このコードはどこでも置けますが、絶対にCoreDataが立ち上がる前に呼ばないといけません。

それでは、CoreDataを立ち上げましょう。これについて2つのポイントがあります。まずはNSPersistentStoreのdescriptionのタイプに、先ほど登録したStringIDを使います。ここはすごく大事です。これを忘れてしまうと、作ったサブクラスは一切使われません。

次にSQLCipherを使っているので、パスワードのオプションを追加します。ここからは、いつもどおりのCoreDataです。呼ぶ場所やオブジェクトの使い方は、気にせずに使いましょう。

NSPersistentStoreサブクラス

ではCoreDataの準備が終わったので、次にSQLCipherを準備しましょう。SQLCipherへのリンクは、SQLCipherのサイトに詳しく書かれているので、ここはあまり細かく話しません。ただ、次の2点はかなり大事なので、気をつけてください。

まず1つは、SQLCipherはSQLiteと一緒に動かないので、SQLiteの代わりにリンクしましょう。もう1つはlinker flagsです。もしSwiftで動かなければ、おそらくlinker flagの問題です。

準備が終わったので、やっとサブクラスを作れます。このスライドが表示しているように実装します。

基本の動きは、この5つのファンクション。loadMetadataで準備して、excute(_:with:)で検索と保存します。2つのnewValueは、データベースから読み込んだデータをNSManagedObjectに変更するためで、obtainPermanentIDs(for:)は、データベースのプライマリーキーを使ってNSManagedObjectIDに作成します。

とりあえず、loadMetadataを見てみましょう。loadMetadataは、CoreDataが起動する時に呼ばれます。やっとStoreをセットアップするタイミングです。

主に3つのタスクがあります。まず1つ目は、データベースを開くことです。

続いて2つ目は、データベースのパスワードを使って、復号化の作業です。先ほどoptionsに追加をしたパスワードを呼んでデータに変更し、UnsafePointerとしてSQLCipherに渡します。

最後に、loadMetadataの名前どおり、metadataをセットしないといけません。基本のvalueはこの2つで、ユニークキーはNSPersistentStoreからそのまま使えます。そしてtypeは先ほどのStringIDになります。

データベースの作成

次は、データベース作成が必要です。基本的にmanagedObjectModelのエンティティを全部テーブル化します。なのでエンティティ、リレーションシップ、そしてインデックスにテーブルを分けます。

まずは、エンティティのテーブルを作りましょう。このテーブルは普通のSQLiteテーブルで、エンティティのプロパティをカラムにマッピングします。

はじめにテーブルのプライマリーキーを指定します。これはNamespace collisionの可能性があるので、名前に気をつけましょう。

次に、エンティティのアトリビュートをカラムに変更します。ここで、attributesByNameを使います。attributesByNameは、NSAttributeDescriptionのディクショナリです。

キーをコードネームとして使い、attributeTypeでSQLiteのタイプを決められます。

toOneとtoMany

そしてエンティティのリレーションシップです。CoreDataでリレーションシップは、2種類あります。toOneとtoManyです。toManyはちょっと特別なので、別のテーブルを作ります。この方法はのちほどお話しします。toOneは、ここで追加できます。ただ、オブジェクトごとに保存はできません。その代わりに、先ほどのプライマリーキーを使います。

ここで止めることはできますが、CoreDataはだいたいsubentityを使いますので、subentityを追加しましょう。CoreDataの裏のsubentityはsuperentityのテーブルに入っているので、同じにしました。今のファンクションがrecursiveになれば、普通に呼んで、コードを書かなくていいので、楽にできます。

そして、カラムの情報が全部集まったので、CREATEを実行して、エンティティテーブルは完成です。

続いて、toManyをどこかでストアしないといけません。なので別のテーブルを作ります。このテーブルのローは、1つのエンティティと1つのリレーションシップです。そこで複数のローを合わせることによって、toManyのリストを作れます。

今一番大事なのは、ここです。toOneはもう使ったので、まずはフィルタリングして、toManyのみにしましょう。そしてテーブルが終わったら、またsubentityのケースを考えないといけません。ただし今度は、同じテーブルではなく、新しいテーブルを作ります。

最後にインデックスを追加しましょう。エンティティにインデックスのリストがあるので、かなり助かります。NSFetchIndexDescriptionを使って、どのカラムをインデックスするか確かめます。また同じテーブルを使っているので、subentityも同じ扱いです。これで、データベース作成が終わりました。

プライマリーキーと検索と保存

データベースを作成したら、プライマリーキーのマッピングが必要です。これは簡単に実装できますので、説明は省略します。

一番大きなプライマリーキーを見つけて、1を足してベースIDとして使います。そのベースIDをnewObjectID(for: referenceObject:)に渡して使います。これは、スーパークラスに実装しているので、呼ぶだけで大丈夫です。

検索と保存は、すべて同じファンクションを通します。少し不思議ですが、AppleのAPIなので仕方ありません。NSPersistentStoreRequestはサブクラスなので、サブクラスのタイプによって検索か保存か分けることは可能です。

検索の場合は、NSFetchRequestですが、保存の場合は、NSSaveChangesRequestです。まずは検索のほうを見てみましょう。

Fetch Requestは、さらに4種類あります。基本はそんなに変わりませんが、全部Arrayをreturnします。ということで、.countResultTypeの場合は、1つのエレメントしか入っていないArrayをreturnします。ここのは、1つのエレメントに1つのオブジェクトのかたちです。

全部の検索をお見せするのは時間的に難しいので、managedObjectIDResultTypeだけを見てみましょう。

まず、さっき作ったプライマリーキーをセレクトします。ここから結果がある間にループして、データベース用のIDを読みます。これはこのまま使いませんので、newObjectID(for: referenceObject:)を通して、NSManagedObjectIDに変更します。あとはreturnするだけです。

ということで検索が終わって、残されているのは保存です。今回は1つのタイプしかないんですが、タイプの中に、3つのタスクが含んでいます。幸いすべて似ているので今日はInsertに集中します。

ちなみに1ヶ所変な行があります。この最後のreturnは、空のArrayです。これはさっきの検索と同じファンクションですから、returnタイプはOptionalじゃない上に、Appleのドキュメンテーションによると、Arrayを期待しています。

とりあえず、Insertを見てみましょう。これはデータベース作成の時とよく似ています。ただし今回の目的はアトリビュートではなく、アトリビュートバリューのことです。今回のほうが、少し難しいです。

まずは、データベースのプライマリーキーを必ず入れないといけないので、先に用意しましょう。続いて、プロパティごとに名前を使って、カラムの名前をマッピングします。バリューのほうはもうちょっとやっかいです。アトリビュートの場合は、そのまま使いますが、リレーションシップの場合はNSManagedObjectなので、直接データベースで使えません。

nilじゃない時に、データベース用のキーに変更する必要があります。ちなみにここでは、toManyを無視していますが、実際にそれもフィルタリングしないといけないんです。ただ、スライドに入れるのはちょっと難しかったです。

アトリビュートとリレーションシップ

では、検索も保存もできたから試してみると、動きません。今のはまだ、Faultしか返せないので、Faultをきちんと読み込まないといけません。

これについては、2つのファンクションがあります。newValuesForObject(with:with:)と、newValue(forRelationship:forObjectWith:with:)です。簡単に言うと、newValuesForObject(with:with:)は、アトリビュートを取り出し、newValue(forRelationship:forObjectWith:with:)は、リレーションシップを取り出します。

はじめに、アトリビュートを見てみます。主に2つのステップがあります。ここで、カラムの名前とタイプを取ります。リレーションシップは、完全に無視しています。先ほどの、newValue(forRelationship:forObjectWith:with:)でできますので、ここはだいぶ楽になります。

これを実装したら、次にvalueをマッピングしないといけません。特別なことはやっていませんが、SQLiteのAPIにより、タイプごとの取り扱いになります。なので、かなり大きい数値になります。

ただし、これが終われば、ストアノードを作って、returnするだけです。

リレーションシップのほうは楽です。1つ引っかかるポイントとしては、このAnyのreturnタイプです。実は3つのタイプをreturnできます。toOneのリレーションシップなら、NSIIncrementalStoreNodeかNSManagedObjectIDにreturnします。そして、toManyの場合は、NSManagedObjectIDのArrayをreturnします。

returnだけではなく、実装も別々なので、toOneとtoManyを分けてみましょう。

toOneの場合は、さっきのアトリビュートより簡単です。リレーションシップをメインエンティティテーブルから取って、NSManagedObjectIDに変更するだけです。毎回同じタイプなので、さっきの大きい数値も必要ありません。

注意点はここです。SQLiteのAPIは、nil Intの場合、0をreturnします。なのでこの実装だと、0をvalid IDとして認めてはいけません。

toManyはtoOneと似ていますが、データベースが別なので、スレッドは少し違います。複数なのでループも入っていますが、それ以外はtoOneとまったく一緒です。

ぜひSNSで連絡を

これで、簡単なStoreを作成できました。いろいろ(説明が)足りない点はありますが、もし興味があればぜひSNSで連絡してください。喜んでお話しします。

今日はiOSと暗号化したCoreDataの作り方という内容をお話ししました。ありがとうございました。

続きを読むには会員登録
(無料)が必要です。

会員登録していただくと、すべての記事が制限なく閲覧でき、
著者フォローや記事の保存機能など、便利な機能がご利用いただけます。

無料会員登録

会員の方はこちら

LINE株式会社

関連タグ:

この記事のスピーカー

同じログの記事

コミュニティ情報

Brand Topics

Brand Topics

  • ランサムウェア攻撃後、わずか2日半でシステム復旧 名古屋港コンテナターミナルが早期復旧できた理由 

人気の記事

人気の記事

新着イベント

ログミーBusinessに
記事掲載しませんか?

イベント・インタビュー・対談 etc.

“編集しない編集”で、
スピーカーの「意図をそのまま」お届け!