骨董品置場

C#とか学んだことかく

.NET でパスワードによるデータの暗号化の実装

書けてないもの書こうぜ!アドベントカレンダー(仮称)

5/5 担当でした。ごめんなさい。たくりんとん君ごめんなさい。失踪してました。


普段私は(このご時世に)自作のパスワード管理ソフトウェアを使っています。

6年くらい前に作った代物なので WinForms で出来ていたりしますが、いろいろ新機能を追加したくなったので思い切って WPF で作り直すことにしました。

UIなど色々未実装の WIP ですが、以下がそのリポジトリです。

github.com

パスワードのデータをアプリ用のマスターパスワードで復号するという仕様です。

セキュアで非同期なイイ感じにしたいなと思い、書いたコードがこれ

暗号化部分

public async Task<Message> EncryptAsync(string password, byte[] rawData, CancellationToken cancellationToken = default)
{
    cancellationToken.ThrowIfCancellationRequested();
    using var deriveBytes = new Rfc2898DeriveBytes(password, saltSize);
    var salt = deriveBytes.Salt;
    using var encAlg = Aes.Create();
    encAlg.Key = deriveBytes.GetBytes(keySize);
    await using var ms = new MemoryStream();
    await using var cs = new CryptoStream(ms, encAlg.CreateEncryptor(), CryptoStreamMode.Write);
    await cs.WriteAsync(rawData, cancellationToken);
    await cs.FlushFinalBlockAsync(cancellationToken);
    var message = new Message
    {
        EncryptedData = ms.ToArray(),
        Iv = encAlg.IV,
        Salt = salt
    };
    return message;
}

Rfc2898DeriveBytes クラスを使うことで、簡単にパスワードベースでキーとソルトを作ることができます。

デフォルトは SHA1 を使ってるけど、SHA256 ハッシュアルゴリズムにした方が良いのかな?)

あとは

  • AES クラスのファクトリメソッドを使って(using var が使えるの最高に気持ちいいですね) オブジェクトを作る。
  • Key を deriveBytes から格納。キーサイズは 16バイト、つまり128ビットに設定しました。AES のデフォルトなはず。
  • Memory に格納したいので MemoryStream を使って非同期に格納します。
  • 暗号化や復号を入出力に置き換えられるCryptoStreamを使って Write して Flush します。 あとは MemoryStream のデータをbyte配列で受け取って、初期化ベクトルとソルトをそれぞれ保管すれば終わり。

ソルトはパスワードをハッシュする際に、初期化ベクトルは暗号化したデータを毎回違う値にするためのもの。

実際にテストで毎回違う値が生成されているのを確認しています。

ソルトや初期化ベクトルは公開しても良いデータなので、そのままメッセージ内に詰めちゃって大丈夫。

これで暗号化部分が完成。

復号部分

public async Task<byte[]> DecryptAsync(string password, Message message, CancellationToken cancellationToken = default)
{
    cancellationToken.ThrowIfCancellationRequested();
    using var deriveBytes = new Rfc2898DeriveBytes(password, message.Salt);
    using var decAlg = Aes.Create();
    decAlg.Key = deriveBytes.GetBytes(keySize);
    decAlg.IV = message.Iv;
    await using var ms = new MemoryStream();
    await using var cs = new CryptoStream(ms, decAlg.CreateDecryptor(), CryptoStreamMode.Write);
    await cs.WriteAsync(message.EncryptedData, cancellationToken);
    await cs.FlushFinalBlockAsync(cancellationToken);
    return ms.ToArray();
}

同じようにRfc2898DeriveBytes クラスを使い、今度はデータから得たソルトを入れて初期化します。

AES クラスの Key, IV プロパティにそれぞれ取得した値を入れて、暗号化時と同じように MemoryStreamCryptoStream を使ってメモリに対して Write, Flush。

最後にメモリの内容を ToArray() で byte[] で取得して後にオブジェクトに直したりして使うといった感じ。

現状イイ感じにデータの暗号化をするならこんな感じかと思っているので備忘録代わりに書いてみました。

それではまた次のアドベントカレンダー記事で。