.NETで暗号化するときの注意点~こうしてあなたは罠に嵌る


はじめに

唐突ですが、今からよく似たコードを2種類提示します。
どちらかが正しく動き、どちらかがランタイムエラーとなります。

A

パターンA.cs
public static class Cipher
{
    private static readonly byte[] InitialzationVector = Encoding.UTF8.GetBytes(@"xxxxxxxxxxxxxxxx");
    private static readonly byte[] SharedKey = Encoding.UTF8.GetBytes(@"xxxxxxxxxxxxxxxx");
    private static readonly int BlockSize = InitialzationVector.Length * 8;
    private static readonly int KeySize = SharedKey.Length * 8;

    public static string Encrypt(string plainText)
    {
        using var rijndael = new RijndaelManaged
        {
            BlockSize = BlockSize,
            KeySize = KeySize,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
            IV = InitialzationVector,
            Key = SharedKey,
        };

        using var encryptor = rijndael.CreateEncryptor();
        var bytes = Encoding.UTF8.GetBytes(plainText);
        return Convert.ToBase64String(encryptor.TransformFinalBlock(bytes, 0, bytes.Length));
    }

    public static string Decrypt(string cipherText)
    {
        using var rijndael = new RijndaelManaged
        {
            BlockSize = BlockSize,
            KeySize = KeySize,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
            IV = InitialzationVector,
            Key = SharedKey,
        };

        using var decryptor = rijndael.CreateDecryptor();
        var bytes = Convert.FromBase64String(cipherText);
        var plain = decryptor.TransformFinalBlock(bytes, 0, bytes.Length);
        return Encoding.UTF8.GetString(plain);
    }
}

B

パターンB.cs
public static class Cipher
{
    private static readonly byte[] InitialzationVector = Encoding.UTF8.GetBytes(@"xxxxxxxxxxxxxxxx");
    private static readonly byte[] SharedKey = Encoding.UTF8.GetBytes(@"xxxxxxxxxxxxxxxx");
    private static readonly int BlockSize = InitialzationVector.Length * 8;
    private static readonly int KeySize = SharedKey.Length * 8;

    public static string Encrypt(string plainText)
    {
        using var rijndael = new RijndaelManaged
        {
            IV = InitialzationVector,
            Key = SharedKey,
            BlockSize = BlockSize,
            KeySize = KeySize,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
        };

        using var encryptor = rijndael.CreateEncryptor();
        var bytes = Encoding.UTF8.GetBytes(plainText);
        return Convert.ToBase64String(encryptor.TransformFinalBlock(bytes, 0, bytes.Length));
    }

    public static string Decrypt(string cipherText)
    {
        using var rijndael = new RijndaelManaged
        {
            IV = InitialzationVector,
            Key = SharedKey
            BlockSize = BlockSize,
            KeySize = KeySize,
            Mode = CipherMode.CBC,
            Padding = PaddingMode.PKCS7,
        };

        using var decryptor = rijndael.CreateDecryptor();
        var bytes = Convert.FromBase64String(cipherText);
        var plain = decryptor.TransformFinalBlock(bytes, 0, bytes.Length);
        return Encoding.UTF8.GetString(plain);
    }
}

結果

答えは Aが正しく動作する でした。
以上!!

...というだけでは寂しいので少しだけ補足をします。

ちなみに違いは BlockSize / KeySize を先に初期化 するか、IV / Keyを先に初期化 するかの違いしかありません。

発生する例外

例外はどうやら Decrypt のときに発生しているようです。

そこで 公式ドキュメント を見に行ってみると、どうやらこの例外は「inputCount パラメーターの長さが、入力ブロック サイズで割り切れない場合」に発生するようです。

じゃあ、実際にどうなっているのか変数をウォッチしてみました。

画像では閉じている項目も比較してみましたが同じ値でした😇

どうやら、ローカルウォッチでは見えないところで何やらあやしいことをしているようです。
闇に片足を突っ込む気がしたので、これ以上は追うのをやめました。

おわりに

プロパティに setter を設ける場合は、利用者側がどういった順序で値を入れていくかは不定なので、こういった設計をしてはいけません。

これは明らかに 悪い設計 なので自分がクラスを作成する際は気をつけましょう。
もし万が一こういった作りをしてしまった場合は、ドキュメントに必ず呼び出し順序の規約を明記しておきましょう。

それがないと誰も仕様がわかりませんし、あなたの考えていることなどエスパーではないので誰も察してくれません。

また、昨今のトレンドで言えばそもそも Immutable にしていこうという流れなので、setter などという諸悪の根源たる機能を使ってはなりません(過激派)。

自戒も込めて、肝に銘じておきましょう。