UnityにSQLCipherを導入して暗号化DBを使う

10 min read読了の目安(約9600字

はじめに

UnityにSQLite-netを導入するの記事でSQLite-netとSQLiteの公式ライブラリを導入するとUnityからSQLiteを使えました。
ここから更にSQLiteをDBを暗号化できるSQLCipherを導入してみます。
SQLCipher自体は有償ですがCommunity EditionはOSSなので各プラットフォーム毎にビルドすると利用できます。

動作環境

以下の環境で動作確認しました。

  • Unity: 2020.3.5f1をIL2CPPビルド
  • OS
    • Windows10
    • Android
    • Intel Mac
    • iOS

SQLCipherを追加

Windows

sqlcipherをビルドする方法を調べるといくつか見付かりますが今はvcpkgに登録されているので環境さえ整備すれば簡単にdllをビルドできます。

登録されているライブラリはコミュニティ主体で更新されるので最新版ではない事もあります。脆弱性修正版が取り込まれていない場合はPort Overlayなどで設定ファイルを修正して最新版の利用をおすすめします。

# 64bit版の場合
vcpkg install sqlcipher:x64-windows

# 32bit版の場合
vcpkg install sqlcipher:x86-windows

ビルドが終わったらそれぞれ以下の2つのdllをUnityプロジェクトにコピーしてImport Settingsを設定します。

  • 64bit版の場合

    installed\x64-windows\bin\libcrypto-1_1-x64.dll
    installed\x64-windows\bin\sqlcipher.dll
    

    コピー先: Plugins/x86_64/

  • 32bit版の場合

    installed\x86-windows\bin\libcrypto-1_1.dll
    installed\x86-windows\bin\sqlcipher.dll
    

    コピー先: Plugins/x86/

Android

Androidはいくつかの導入方法があり、Gradleを利用、直接追加、自前ビルドの3つを紹介します。

Gradleを利用

公式ライブラリがmaven repositoryに登録されているのでドキュメント[1]と同じようにgradleを利用します。
この方法はビルド時にダウンロードするのでプロジェクトに追加しなくてもよいのが利点です。

  1. Project Settings > Publishing SettingsのCustom Main Gradle TemplateCustom Gradle Properties Templateをチェックします。

  2. Plugins/Android/mainTemplate.gradleのdependenciesにライブラリを追加します。

    --- a/Assets/Plugins/Android/mainTemplate.gradle 2021-04-25 15:08:06.515926300 +0900
    +++ b/Assets/Plugins/Android/mainTemplate.gradle 2021-04-19 20:38:55.095754500 +0900
    @@ -5,6 +5,8 @@
    
     dependencies {
         implementation fileTree(dir: 'libs', include: ['*.jar'])
    +    implementation 'net.zetetic:android-database-sqlcipher:4.4.3@aar'
    +    implementation "androidx.sqlite:sqlite:2.0.1"
     **DEPS**}
    
     android {
    
  3. AndroidX[2]を使っているのでPlugins/Android/gradleTemplate.propertiesにandroid.useAndroidX=trueを追加します。

    --- a/Assets/Plugins/Android/gradleTemplate.properties 2021-04-25 15:17:57.327076200 +0900
    +++ b/Assets/Plugins/Android/gradleTemplate.properties 2021-04-25 15:17:35.993821900 +0900
    @@ -1,5 +1,6 @@
     org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M
     org.gradle.parallel=true
     android.enableR8=**MINIFY_WITH_R_EIGHT**
    +android.useAndroidX=true
     unityStreamingAssets=.unity3d**STREAMING_ASSETS**
     **ADDITIONAL_PROPERTIES**
    

ライブラリを直接追加

この方法はGradleが分からなくてもとりあえず追加すればよいのが利点です。

  1. 以下のページからそれぞれライブラリをダウンロードします。
  2. Plugins/Android/にコピーしてImport Settingsを設定します。

自前ビルド

https://github.com/sjemens/sqlcipher のandroidブランチにビルドスクリプトがあるのでこれを利用するとJNIなしのlibsqlcipher.soを簡単にビルドできます。ですが公式ライブラリと比べてapkが100kb程度しか小さくならないのであまり利点がありません。

macOS/iOS

macOS/iOSはいくつかの導入方法があり、自前ビルド、CocoaPodsの2つを紹介します。

自前ビルド

https://github.com/jfcontart/SqlCipher4Unity3D_Apple にビルドスクリプトと野良バイナリがあるのでこれを利用できます。ですが非公式の配布ライブラリなので安全を取るなら自前ビルドをおすすめします。

ライブラリをUnityプロジェクトにコピーしたらImport Settingsを設定します。

  • Plugins/macOS/sqlcipher.bundleの場合
  • Plugins/iOS/libsqlcipher.aの場合

CocoaPodsを利用

公式ライブラリがCocoaPodsに登録されているのでドキュメント[3]と同じようにiOSの場合はXCodeプロジェクトを出力した後にCocoaPodsを利用できます。

  1. XCodeプロジェクトにPodfileを追加する

    Podfile
    target 'UnityFramework' do
      pod 'SQLCipher', '~>4.0'
    end
    
  2. podをインストールする

    pod install
    
  3. <ProductName>.xcworkspaceからXCodeを開いてビルドする

SQLite-netをSQLCipherに対応する

SQLite-netからsrc/SQLite.cssrc/SQLiteAsync.csをコピーして以下の4つを修正します。

  • パスワード設定クエリ対応[4]
  • パスワード変更メソッド追加
  • iOS向けライブラリ名対応
  • Windows専用関数対応
--- a/src/SQLite.cs   2021-04-25 16:24:30.733651600 +0900
+++ b/src/SQLite.cs   2021-04-25 16:50:37.393322000 +0900
@@ -370,7 +370,7 @@
                        if (key == null)
                                throw new ArgumentNullException (nameof (key));
                        var q = Quote (key);
-                       Execute ("pragma key = " + q);
+                       ExecuteScalar<string> ("pragma key = " + q);
                }

                /// <summary>
@@ -387,7 +387,25 @@
                        if (key.Length != 32)
                                throw new ArgumentException ("Key must be 32 bytes (256-bit)", nameof (key));
                        var s = String.Join ("", key.Select (x => x.ToString ("X2")));
-                       Execute ("pragma key = \"x'" + s + "'\"");
+                       ExecuteScalar<string> ("pragma key = \"x'" + s + "'\"");
+               }
+
+               public void SetReKey (string key)
+               {
+                       if (key == null)
+                               throw new ArgumentNullException (nameof (key));
+                       var q = Quote (key);
+                       ExecuteScalar<string> ("pragma rekey = " + q);
+               }
+
+               public void SetReKey (byte[] key)
+               {
+                       if (key == null)
+                               throw new ArgumentNullException (nameof (key));
+                       if (key.Length != 32)
+                               throw new ArgumentException ("Key must be 32 bytes (256-bit)", nameof (key));
+                       var s = String.Join ("", key.Select (x => x.ToString ("X2")));
+                       ExecuteScalar<string> ("pragma rekey = \"x'" + s + "'\"");
                }

                /// <summary>
@@ -4356,7 +4374,11 @@
                        Serialized = 3
                }

-               const string LibraryPath = "sqlite3";
+#if UNITY_IOS && !UNITY_EDITOR
+               const string LibraryPath = "__Internal";
+#else
+               const string LibraryPath = "sqlcipher";
+#endif

 #if !USE_CSHARP_SQLITE && !USE_WP8_NATIVE_SQLITE && !USE_SQLITEPCL_RAW
                [DllImport(LibraryPath, EntryPoint = "sqlite3_threadsafe", CallingConvention=CallingConvention.Cdecl)]
@@ -4392,8 +4414,10 @@
                [DllImport(LibraryPath, EntryPoint = "sqlite3_config", CallingConvention=CallingConvention.Cdecl)]
                public static extern Result Config (ConfigOption option);

+#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
                [DllImport(LibraryPath, EntryPoint = "sqlite3_win32_set_directory", CallingConvention=CallingConvention.Cdecl, CharSet=CharSet.Unicode)]
                public static extern int SetDirectory (uint directoryType, string directoryPath);
+#endif

                [DllImport(LibraryPath, EntryPoint = "sqlite3_busy_timeout", CallingConvention=CallingConvention.Cdecl)]
                public static extern Result BusyTimeout (IntPtr db, int milliseconds);

SQLite-netでパスワードを設定する

SQLCipherで暗号化DBを作る方法と従来のSQLiteでDBを作る方法の比較です。
SQLCipherではoptionでDBのパスとパスワードを設定しています。

パスワードはnullと32バイトのbyte[]とstringを指定可能で、nullはパスワードなしと同じ、32バイトのbyte[]はstringより速くなります。[5][6]

// SQLCipherでパスワードを設定する場合
var path = Path.Combine(Application.persistentDataPath, "database");
var password = "your-secret-password";
var option = new SQLiteConnectionString(database, true, password);
var db = new SQLiteConnection(option);
// 従来のSQLiteと同じパスワードなしの場合
var path = Path.Combine(Application.persistentDataPath, "database");
var db = new SQLiteAsyncConnection(path,
    SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex | SQLiteOpenFlags.ReadWrite);

パスワードを変更する場合は追加したSetReKeyメソッドを使います。

// DBオープンと同時にパスワードを変更する場合
var path = Path.Combine(Application.persistentDataPath, "database");
var password = "old-secret-password";
void ReKey(SQLiteConnection conn)
{
    conn.SetReKey("new-secret-password");
}
var option = new SQLiteConnectionString(database, true, password, postKeyAction: ReKey);
var db = new SQLiteConnection(option);
// DBオープン後にパスワードを変更する場合
var path = Path.Combine(Application.persistentDataPath, "database");
var password = "old-secret-password";
var option = new SQLiteConnectionString(database, true, password);
var db = new SQLiteConnection(option);

// dbを開いている間ならいつでも可能
db.SetReKey("new-secret-password");

余談

本記事で導入したライブラリと同様のアセットにSQLCipher4Unity3Dがあります。違いは以下の3点です。

  • sqlite-netのSQLite.csをclassやenum毎にファイル分割
  • 各プラットフォームのネイティブラリブラリ同梱(バージョンは若干古い4.4.0)
  • サンプルシーンとスクリプト同梱

sqlcipher4系は4.4.1以前と4.4.2以前に脆弱性[7][8]があるのでそのままの利用はおすすめできません。ですがサンプルシーンとスクリプトは実装の参考になると思います。

脚注
  1. https://www.zetetic.net/sqlcipher/sqlcipher-for-android/ ↩︎

  2. https://developer.android.com/jetpack/androidx ↩︎

  3. https://www.zetetic.net/sqlcipher/ios-tutorial/ ↩︎

  4. https://github.com/praeclarum/sqlite-net/pull/1026 ↩︎

  5. https://qiita.com/atr-toru/items/d98a434eecf9f58c443d ↩︎

  6. https://www.zetetic.net/sqlcipher/sqlcipher-api/#key ↩︎

  7. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-27207 ↩︎

  8. https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3119 ↩︎