前回までの記事で、SQLCipher を使って暗号化した SQLite の使い方について説明をしました。
- Unity SQLiteUnityKit 暗号化 SQLCipher (SQLite) を Android で利用する(.so コンパイル)
- Unity SQLiteUnityKit 暗号化 SQLCipher (SQLite) を Windows で利用する(DLL コンパイル)
SQCipher のデータベースにアクセスするときには、必ずパスワードを入力・設定する必要があります。
というわけで、今回はこのパスワードの管理について、(個人的に)まとめた記事です。
SQLCipher にオススメのパスワード形式
SQLCipher の「Setting the Key」に説明がありました。
SQLCipher にわたすパスワード(パスフレーズ)を、どこまで SQLCipher まかせにするかといったところで、アクセス速度に差がでます。
パスワードは、そのテキストのまま利用しているわけではなくて、1から3の方向にハッシュ化したり、ソルトを付与したり、ストレッチング(イテレーション)を与えて、最終的なパスワード(ハッシュ値)を割り出しにくくしています。
- abcdef
自由なテキストで表現されたパスワード - 2DD29CA851E7B56EB0E1F...(64 文字)
16 進数で表現された 64 文字のテキストのパスワード - 2DD29CA851E7B56EB0E1F...(64 文字) + 101010...(32 文字)
16 進数で表現された 64 文字と、sait として利用する 2 進数で表現された 32 文字のテキストの組み合わせのパスワード
この流れからわかるとおり、1ほど SQLCipher が内部でパスワードを処理するコストが重く、2、3ほどコストが小さくなります。ここから先はケース・バイ・ケースとなりますが、個人的には「2」がベターな選択じゃないかと考えました。
なので、2を想定したパスワードのハッシュ化の方法についても記載しています。
1は、SQLCipher のおまかせ管理。でも、DB アクセスのコストが高く(ハッシュ化のストレッチングが 64,000 回)、生パスワードの管理が求められるのも違和感があります。
2は、アクセスコストが改善するし、パスワードのハッシュ化を(自分で)やる。チート対策には具合がいいように思います。一応、どの程度が適切なのかは、要件(ゲームがどの程度の速度・セキュリティを必要とするか)と、実例から判断するとよいと思います。(CRYPTREC 暗号リストなども)
ハッシュ化する仕組みの例
今回は、以下のような要件を満たすパスワードハッシュ化の仕組みを用意することにしました。素早いハッシュ化の要件を満たすために、ストレッチングの回数を調整する感じです。(実行環境にも依るところなので、参考値程度に)
- ハッシュ化アルゴリズムは PBKDF2 を採用する(※NIST が認めているもの)
- salt を付与すること
- ストレッチングはするけど、負荷を考慮して軽い回数 (OWASP は 10,000 回以上を推奨してるけど1)
- ハッシュ化に要する時間は 50ms 以下
salt の生成
salt もパスワードと同じようにプログラムが管理します。OWASP では 32/64 byte のサイズが推奨みたいですので、64 文字の salt の値を生成しておきます。
こんな感じで 64 byte の雑な salt を用意することにしました。
public static class Program { public static void Main() { var salt = "Y7tl32r32we9X1FkwamTttjWsK6gPgjbEJX0mdWNwjW825JgAOo5FLNB22JqGQpr"; Console.WriteLine(System.Text.Encoding.UTF8.GetBytes(salt).Length); } }
ハッシュ化するためのクラス(サンプル)
ハッシュ化するための部分は「Rfc2898DeriveBytes」クラスにおまかせ。
sing System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; /// <summary> /// <see cref="Hash" /> クラスは、任意のテキスト値から固定長のハッシュ値に変換するクラスです。 /// <para> /// salt 値は 8 バイト以上、演算の反復処理回数は 1 回以上を指定する必要があります。 /// </para> /// </summary> public class Hash { #region Fields private int _Iteration = 1024; #endregion #region Initializes /// <summary> /// <see cref="Hash"/> クラスの新しいインスタンスを初期化します。 /// </summary> public Hash() { } #endregion #region Public Methods /// <summary> /// PBKDF2 に基づくハッシュ値を取得します。 /// </summary> /// <param name="password">ハッシュ値を得るパスワード。</param> /// <param name="salt">ハッシュ値を派生させるために使用する salt の値。</param> /// <param name="size">生成するハッシュ値のバイト数</param> /// <returns>ハッシュ値を格納したバイト配列。</returns> /// <exception cref="ArgumentNullException">指定したパスワードは null または空白です。</exception> /// <exception cref="ArgumentNullException">指定した salt は null または空白です。</exception> /// <exception cref="ArgumentException">指定された salt のサイズが 8 バイト未満です。</exception> /// <exception cref="InvalidOperationException">反復処理回数は 1 回以上を指定してください。</exception> public byte[] GetPbkdf2(string password, string salt, int size = 32) { if (string.IsNullOrEmpty(password)) { throw new ArgumentNullException("指定したパスワードは null または空白です。"); } if (string.IsNullOrEmpty(password) || string.IsNullOrEmpty(salt)) { throw new ArgumentNullException("指定した salt は null または空白です。"); } var saltBytes = Encoding.UTF8.GetBytes(salt); if ((saltBytes?.Length ?? 0) <= 8) { throw new ArgumentException("指定された salt のサイズが 8 バイト未満です。"); } if (_Iteration <= 0) { throw new InvalidOperationException("反復処理回数は 1 回以上を指定してください。"); } return new Rfc2898DeriveBytes(password, saltBytes, _Iteration)?.GetBytes(size); } /// <summary> /// 演算の反復処理回数を変更します。 /// </summary> /// <param name="iteration">演算の反復処理回数。</param> /// <exception cref="ArgumentOutOfRangeException">反復処理回数の回数は 1 回以上を指定してください。</exception> public void ChangeIteration(int iteration) { if (iteration <= 0) { throw new ArgumentOutOfRangeException("反復処理回数の回数は 1 回以上を指定してください。"); } _Iteration = iteration; } #endregion }
ただ、これだけだと使いづらいので、期待するハッシュテキストにアダプトしてくれるような生成用クラスを作成します。
/// <summary> /// <see cref="HashGenerator"/> クラスは、パスワードのハッシュ値を生成するクラスです。 /// </summary> public class HashGenerator { #region Fields private static string _Salt = "qqWsf2yqy7vuZ9BDdwv0mlLnbhoTS6vw9Pr0NU5tD1aneMyydEv82lIxykeepfZ8"; private static Hash _Hash = new Hash(); #endregion #region Initializes /// <summary> /// <see cref="HashGenerator"/> クラスの新しいインスタンスを初期化します。 /// </summary> public HashGenerator() { } #endregion #region Public Methods /// <summary> /// ハッシュ値を派生させるために使用する salt 値を変更します。 /// <para> /// salt 値は 64 文字以上を設定する必要があります。 /// </para> /// </summary> /// <param name="salt">変更する salt 値。</param> /// <exception cref="ArgumentException">salt の文字数は 64 文字以上を入力してください。</exception> public static void ChangeSalt(string salt) { if (salt.Length < 64) { throw new ArgumentException("salt の文字数は 64 文字以上を入力してください。"); } _Salt = salt; } /// <summary> /// 指定したパスワードのハッシュ値を表すテキストを生成します。 /// </summary> /// <param name="password">パスワードを表すテキスト。</param> /// <param name="length">ハッシュ値を表すテキストの文字数。</param> /// <returns>ハッシュ値を表すテキスト。</returns> /// <exception cref="ArgumentNullException">パスワードのテキストが空白です。</exception> /// <exception cref="ArgumentOutOfRangeException">ハッシュ値の長さは 1 以上を指定する必要があります。</exception> /// <exception cref="InvalidOperationException">ハッシュ値の生成に失敗しました。</exception> public static string Generate(string password, int length = 64) { if (string.IsNullOrEmpty(password)) { throw new ArgumentNullException("パスワードのテキストが空白です。"); } if (length <= 0) { throw new ArgumentOutOfRangeException("ハッシュ値の長さは 1 以上を指定する必要があります。"); } var hash = _Hash.GetPbkdf2(password, _Salt, length / 2); if (hash == null || hash.Length != (int)(length / 2)) { throw new InvalidOperationException("ハッシュ値の生成に失敗しました。"); } var hashText = string.Join("", hash.Select(p => Convert.ToByte(p).ToString("x2"))); // 奇数の場合は末尾に既定の文字 0 を追加する if (length % 2 != 0) { hashText += "0"; } return hashText; } #endregion }
実際つかうときは、こう。らくちんですね。
private void Run() { var hashedPassword = HashGenerator.Generate("password"); Console.WriteLine(hashedPassword); }
テスト
最後に、要件をきちんと満たしているかどうかチェックするテストを用意しておきます。たとえばこんなのです。
[TestCase("wdDsJwK_uDdCi@")] [TestCase("rance_sill_athena")] [TestCase("password")] [Timeout(50)] public void Test_ハッシュテキストの文字数チェック(string password) { var hash = HashGenerator.Generate(password); // 64 文字であることをチェック Assert.AreEqual(hash.Length, 64); }
問題なし、想定外の使い方をしたら例外を投げるのもチェック。
Unity でもコードを動かせるかチェック。問題なさそうです。
サンプル
今回のプログラムを GitHub に公開しています。
Samples のリポジトリーは、過去に作成したサンプルプログラムをまとめて公開しています。今回のプログラムは「EncryptHashSample」です。
参考
- パスワードはハッシュ化するだけで十分?
- Qiita - Gitのコミットハッシュ値はどうやって生成されているのか
- パスワードハッシュ化で用いるソルト(Salt)とペッパー(Pepper)/シークレットソルト(Secret Salt)の役割と効果
- cocos2d-x ローカルデータの暗号化、SQLCipherを爆速化する方法
Pythonで学ぶアルゴリズムとデータ構造 (データサイエンス入門シリーズ)
- 作者:辻 真吾
- 発売日: 2019/11/28
- メディア: 単行本(ソフトカバー)