これは ドリコム Advent Calendar 2019の11日目です。
10日目は 廣田 洋平 さんによる、Ubuntu on Arch Linux on Raspberry Pi で Webアプリを動かしてみよう です。

SELECT しろよ。平文は既に・・・『暗号文』に変わってるんだぜ

こんにちは、 Smith (@do_low) です。
ドリコムにはゲーム事業がありますが、パートナー様からキャラクターやゲームシステムなどの IP をお預りしてゲームプロダクトを開発する、という案件がしばしばあります。
そのため、ゲーム画面に表示する画像や動画、楽曲やボイスデータなどは悪意のあるユーザから守らなくてはならず、マスターデータの類も例外ではありません。
悪意のあるユーザがアプリを解析し、未公開のシナリオなどを SNS 上で拡散するなどした場合、ユーザ様がアプリを遊ぶ楽しみを損なうこともあります。
またアプリ提供側としてもガチャのキャラクター追加などの予測がされ、人気キャラが配信されるまでの買い控えが生じる、などの被害も考えられます。

ゲームアプリにおけるコンテンツの保護には暗号化手法を用いることも多いと思いますが、一口に暗号化をする、と言ってもそのレイヤーは通信経路やコンテンツ、ローカルストレージでの永続化状態やオンメモリの状態などのいくつかに分かれます。
本稿ではこのうち、マスターデータが永続化されるローカルストレージにフォーカスした内容となります。


図: いくつかの暗号化ポイント、 Local Storage も含まれる

前提条件

  • Unity でのゲーム開発
  • ローカルストレージに sqlite3 が採用されている

吐き気をもよおす『邪悪』とはッ!!なにも索引できぬデータを保存することだ・・・!!

永続化された情報を秘匿するために暗号化手法を用いる場合、データベースの入出力について暗号化/復号を行う必要があります。
(データベースそのものを暗号化するという選択肢もありますが、ゲームで扱うマスターデータのサイズは肥大するため実用的ではないと判断できます)
sqlite3 は C で書かれたライブラリであるため、 Unity C# 層からはいわゆるアンマネージド層として扱われ、ネイティヴプラグインの API 呼び出しを必要とします。
素直に入出力の値を暗号化/復号するのであれば、マネージド層である C# にてその処理を行えばデータの秘匿という目的は達成されます。
しかしながら sqlite3 は RDBMS であるため、暗号化した値が保存されているとその検索性は失われます。


図: 暗号化後の値でしか索引できない問題

何よりも『困難』で・・・VFS 無くしては近づけない道程だった・・・

そこで、 sqlite3 にて提供されている VFS と称される仕組みを試してみることにしました。
https://www.sqlite.org/vfs.html

VFS は、アプリケーションが DB 操作を行う際のファイルシステム操作をフックする層で、 open/close/read/write などのあらゆる操作について任意の処理を施すことができます。
これはアプリケーションからの直接の操作のみではなく sqite3 自身がアプリケーションが必要とする操作を完遂するために必要なファイル操作も対象となります。
そのため、この層で暗号化/復号処理を行うことができれば、値は暗号化されたままで保存される上に、復号もファイル操作の層で行われるため、検索やソートなどが従来どおり行えるようになります。


図: VFS層の実装により sqlite3 層では復号済みの情報を扱える

自分の「ユースケース」は・・・自分が決めるんだ・・・

複数プラットフォームでの動作

Unity で開発する以上は多様なプラットフォームで動作させられなければなりません。
ゲームが動作する iOS/Android は当然ですが、開発者の環境である Windows/Mac も対応させなければなりません。
これは sqlite3 に限ったことではありませんが、多くの場合、ネイティヴプラグインでは複数プラットフォーム対応の開発コストが跳ね上がります。
今回の VFS 層の開発だと、 Windows のファイルシステム操作をガッツリ行わなければならなかったのですが、社内に経験者がいないこともあり、対応のコストが比較的高かったパートです。

static off_t drecom_file_seek(drecom_sqlite3_vfs_ctx *ctx, sqlite_int64 offset, const int where)
{
#if defined(DR_ARCH_WIN)
    LARGE_INTEGER result;
    LARGE_INTEGER position;
    position.LowPart = (DWORD)offset;
    position.HighPart = (DWORD)(offset >> 32);
    if (SetFilePointerEx(ctx->fd, position, &result, where) == 0)
        return -1;
    return (((long long)result.HighPart) << 32) + result.LowPart;
#else
    return lseek(ctx->fd, offset, SEEK_SET);
#endif
}

リスト: かなり自信のない Windows 実装例

sqlite3 データ構造

sqlite3 データ構造 においては 0x00 という値(ヌルバイト)がデータ構造上の意味を持っているため、この値まで含めて暗号化すると正しく db を扱う事ができませんでした。
暗号化シーケンスや暗号アルゴリズムによっては 0x00 を維持したまま暗号化することも可能ですが、0x00 が出現する頻度や間隔などで秘匿性が下がる可能性もあります。
どのみち、ファイルへの書き込み対象の範囲を素直に暗号化することはできないため、書き込み時にデータの表現方法を変えたり、暗号アルゴリズムをブロック暗号ではないものに変えるなどの工夫が必要と考えられます。


図: 素直にブロック暗号を用いると 0x00 も区別なく暗号化し、予期せぬ 0x00 も生まれる

パフォーマンス

パフォーマンス面では、当然のことながら平文で扱うよりもオーバーヘッドが発生しますが、これは入出力の値の長さが長くなるにつれて顕著になります。
マスターデータが json で管理されている等の場合、json 文字列そのものを保存したり、シリアライズしたバイト配列を保存するよりも、RDB テーブルのレコードとして保存しなおして単一のデータエンティティのサイズを小さくするなどの工夫が必要な場面がありそうです。


課題は諸々ありますが、ここから先はトレードオフの関係にあったり、使う側の戦略次第だったりするので、ユースケースに合わせられるように必要十分の機能で固めるのが良いでしょう。
フレームワークのデザインは、使う側に寄り添わなければ悪しきものになりがちなので、基礎開発時点でやりすぎるのは禁物です。

まとめ

本稿では Unity x sqlite3 という前提条件の元でデータの秘匿を行う、ということについて考えてみました。

  • sqlite3 VFS を実装すれば、「データの秘匿」と「検索性の保持」の「両方」ができる幹部になれる
  • Windows などの複数プラットフォーム対応には、暗闇の荒野に道を切り開く「覚悟」が必要
  • sqlite3 データ構造上、素直なブロック暗号/復号はできない
  • パフォーマンスはトレードオフになる
  • データの保存の仕方など、ユースケースに応じて戦略を立てるべし

細かい事抜きにして、こういう技術があるよってことと、触るとそこそこ楽しいよ、ってことがなんとなく伝われば幸いです。
ではまた来年、アリーヴェデルチ!

明日は nemoto_yusuke さんの記事です。

ドリコムでは一緒に働くメンバーを募集しています! 募集一覧はコチラを御覧ください!


初稿の記載内容に誤りがあり、下記のように訂正いたしました、深くお詫び申し上げます。
誤: アリーヴェ・デルチ
正: アリーヴェデルチ