文字列の暗号化/復号化

C#での暗号化と復号化について説明します。

対称暗号化(AES)

対称暗号化とは、暗号化と復号化を行う際同じキーを使って処理を行う方式です。
米国政府の暗号標準であるAES(Advanced Encryption System)の暗号アルゴリズムに採用された共通鍵(秘密鍵)を使用して暗号化を行います。

IV(初期化ベクトル:initialization vector)と共通鍵(秘密鍵)を使って暗号を行います。

よくIV固定で暗号化をしているソースコードを見かけますが、セキュリティの面であまりよくないとされています。
IVと共通鍵が同じ場合、出力される結果が毎回同じになるため危ういです。

初期化ベクトルは、暗号化の結果を毎回変えるためだけに使われるデータであり、初期化ベクトルから元のデータを予測することはできません。そのため、初期化ベクトルは第三者に知られても問題ありません。

それを踏まえ以下の暗号化/復号化の関数を作成しました。

暗号化関数

先頭に以下のnamespaceを追加してください。

using System.Security.Cryptography;

今回は、AES CNGで作りました。よくRijndaelManagedで暗号化しているプログラムを見かけますが、RijndaelおよびRijndaelManagedは廃止されました。いずれ使えなくなる可能性があります。

/// <summary>
/// 対称鍵暗号を使って文字列を暗号化する
/// </summary>
/// <param name="text">暗号化する文字列</param>
/// <param name="key">共有鍵</param>
/// <param name="iv">(out)初期化ベクトル</param>
/// <returns>暗号化された文字列</returns>
public static string EncryptAesCng(string text, string key, out string iv)
{
    //Advanced Encryption Standard (AES) アルゴリズムの Cryptography Next Generation (CNG) 実装        

    using (AesCng aes = new AesCng())
    {
        aes.BlockSize = 128;                //ブロックサイズ(Bit)
        aes.KeySize = 128;                  //キーサイズ(Bit)・・・有効なキーサイズは 128、192、および 256 ビット
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;

        aes.GenerateIV();
        iv = System.Convert.ToBase64String(aes.IV);

        aes.Key = Encoding.UTF8.GetBytes(key);

        ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        // 対象の文字列をバイトデータに変換
        var byteValue = Encoding.UTF8.GetBytes(text);

        // バイトデータの長さを取得
        var byteLength = byteValue.Length;

        //暗号化処理
        byte[] encrypted = encryptor.TransformFinalBlock(byteValue, 0, byteLength);

        return (System.Convert.ToBase64String(encrypted));
    }
}

KeySizeは、bitで書きます。
なのでこの場合128bitなのでBYTEに直すと128 ÷ 8 = 16BYTEということになります。半角英数字は1文字1BYTEなので16文字のKey(共通鍵)を用意する必要があります。

初期ベクトルがないと復号化できません。暗号化された文字列と合わせて初期ベクトルも復号化する側に伝える必要があります。
先ほど記述しましたが、初期ベクトルは他のだれかに知られても復号化することはできません。

復号化関数

暗号化された文字列、初期化ベクトル、共通鍵を渡して復号化します。

/// <summary>
/// 対称鍵暗号を使って暗号文を復号する
/// </summary>
/// <param name="cipher">暗号化された文字列</param>
/// <param name="iv">初期化ベクトル</param>
/// <param name="key">共有鍵</param>
/// <returns>復号された文字列</returns>
public static string DecryptAesCng(string cipher, string iv, string key)
{
    using (AesCng aes = new AesCng())
    {
        aes.BlockSize = 128;
        aes.KeySize = 128;
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;

        //初期ベクトルと共通鍵をセット
        aes.IV = System.Convert.FromBase64String(iv);
        aes.Key = Encoding.UTF8.GetBytes(key);

        ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

        //暗号化文字列をバイト配列に戻す
        byte[] encypted = System.Convert.FromBase64String(cipher);  

        //復号化処理
        byte[] plains = decryptor.TransformFinalBlock(encypted, 0, encypted.Length);

        //バイト配列を文字列に戻す
        return Encoding.UTF8.GetString(plains);
    }
}

サンプル

上記の暗号化/復号化の関数を使ってサンプルプログラムを作成しました。

string iv;                          //初期ベクトル
string key = "8nCcT7wVXdRWicxz";    //KeySize = 128なので16文字
string text = "テスト用文字列";     //暗号化する文字列

Console.WriteLine($"  対象文字列:{text}");

//暗号化
string encrypt= EncryptAesCng(text, key, out iv);

Console.WriteLine($"暗号化文字列:{encrypt}");
Console.WriteLine($"初期ベクトル:{iv}");

//復号化
string plain = DecryptAesCng(encrypt, iv, key);

Console.WriteLine($"復号化文字列:{plain}");

実行結果は以下です。

非対称暗号化

非対称暗号化とは、素因数分解の難解さを利用して暗号化を行う方式です。
暗号化するための公開鍵復号化するための秘密鍵の異なる鍵のペアになっています。

秘密鍵をもった人が暗号化した文字列を戻せるので、秘密鍵は公開してはいけません。

公開鍵と秘密鍵を作成する関数

まずは暗号化、復号化するための鍵ファイルを作成します。実際の環境では他社に公開鍵を渡すことを踏まえファイル出力しています。

CommonOpenFileDialog を利用するために「WindowsAPICodePack-Shell」を参照しています。
参照方法はこちら

/// <summary>
/// 公開鍵と秘密鍵のファイル作成する
/// </summary>
public static bool CreateKeys()
{
    //RSACryptoServiceProviderオブジェクトの作成
    using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
    {

        string output_dir = "";

        //フォルダを指定してそこに公開鍵と秘密鍵を出力する
        using (CommonOpenFileDialog dlg = new CommonOpenFileDialog())
        {

            // フォルダ選択ダイアログ(falseにするとファイル選択ダイアログ)
            dlg.IsFolderPicker = true;

            if (dlg.ShowDialog() != CommonFileDialogResult.Ok)
            {
                //開く以外を選択された場合はfalseを返す。
                return false;
            }

            //選択したフォルダを取得する
            output_dir = dlg.FileName;

        }

        //公開鍵をXML形式で取得
        string public_key = rsa.ToXmlString(false);

        //公開鍵をファイル出力
        using (StreamWriter sw = new StreamWriter(output_dir + @"\public_key.xml", false, Encoding.UTF8))
        {
            sw.Write(public_key);
        }

        //秘密鍵をXML形式で取得
        string private_key = rsa.ToXmlString(true);

        //秘密鍵をファイル出力
        using (StreamWriter sw = new StreamWriter(output_dir + @"\private_key.xml", false, Encoding.UTF8))
        {
            sw.Write(private_key);
        }

    }

    return true;
}

公開鍵を使用して暗号化する

出力した公開鍵ファイルを使って暗号化します。今回は公開鍵ファイルを他者に渡しているという想定で暗号化関数を作成しました。

/// <summary>
/// 公開鍵で文字列を暗号化する
/// </summary>
/// <param name="text">平文の文字列</param>
/// <param name="encrypted">暗号化された文字列</param>
/// <returns>true:暗号化成功、false:暗号化失敗</returns>
public static bool EncryptRSA(string text, out string encrypted)
{
    string public_key_file = "";
    
    //公開鍵を読込む
    using (OpenFileDialog dlg = new OpenFileDialog())
    {
        dlg.Filter = @"公開鍵(*.xml)|*.xml";
        dlg.Multiselect = false;

        if (dlg.ShowDialog() != DialogResult.OK)
        {
            encrypted = "";
            return false;
        }

        //公開鍵のファイルを取得する
        public_key_file = dlg.FileName;
    }

    string public_key = "";

    //公開鍵を読込む
    using (StreamReader sr = new StreamReader(public_key_file, Encoding.UTF8))
    {
        public_key = sr.ReadToEnd();
    }

    try
    {
        //公開鍵を使って暗号化
        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            rsa.FromXmlString(public_key);

            byte[] data = Encoding.UTF8.GetBytes(text);

            data = rsa.Encrypt(data, false);

            encrypted = Convert.ToBase64String(data);

            return true;
        }

    }
    catch(Exception ex)
    {
        MessageBox.Show(ex.Message);
        encrypted = "";
        return false;
    }
}

秘密鍵を使用して復号化する

/// <summary>
/// 秘密鍵で暗号文を復号化する
/// </summary>
/// <param name="cipher">暗号化された文字列</param>
/// <param name="plain">復号された文字列</param>
/// <returns>true:復号化成功、false:復号化失敗</returns>
public static bool DecryptRSA(string cipher, out string plain)
{
    string private_key_file = "";

    //秘密鍵を読込む
    using (OpenFileDialog dlg = new OpenFileDialog())
    {
        dlg.Filter = @"秘密鍵(*.xml)|*.xml";
        dlg.Multiselect = false;

        if (dlg.ShowDialog() != DialogResult.OK)
        {
            plain = "";
            return false;
        }

        //秘密鍵のファイルを取得する
        private_key_file = dlg.FileName;
    }

    string private_key = "";

    //秘密鍵を読込む
    using (StreamReader sr = new StreamReader(private_key_file, Encoding.UTF8))
    {
        private_key = sr.ReadToEnd();
    }

    try
    {
        using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
        {
            rsa.FromXmlString(private_key);

            byte[] data = Convert.FromBase64String(cipher);

            data = rsa.Decrypt(data, false);

            plain = Encoding.UTF8.GetString(data);

            return true;
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
        plain = "";
        return false;
    }

}

サンプル

非対称暗号で行うサンプルです。

string text = "非対称暗号化のテスト";
string encrypt = "";
string plain = "";

//公開鍵、秘密鍵のファイル作成
CreateKeys();

Console.WriteLine($"  対象文字列:{text}");

//暗号化
if (EncryptRSA(text, out encrypt) == false)
{
    Console.WriteLine("暗号化失敗");
}

Console.WriteLine($"暗号化文字列:{encrypt}");

//復号化
if (DecryptRSA(encrypt, out plain) == false)
{
    Console.WriteLine("復号化失敗");
}

Console.WriteLine($"復号化文字列:{plain}");

実行結果

まとめ

暗号化は、外部との通信をする際データを守る重要な役割があります。

また今回の処理では文字列をByte配列にしているということは、どんなデータもByte配列にできれば暗号化できるということに気づきます。

また別の記事で画像などのバイナリデータを暗号化する方法を記事にしようと思います。

この記事が皆様のお役に立てたら幸いです。

タイトルとURLをコピーしました