1. ホーム
  2. c#

[解決済み】C#で文字列の暗号化・復号化【重複あり

2022-04-15 09:08:50

質問

C#で以下を満たす最も現代的な(最良の)方法は何でしょうか?

string encryptedString = SomeStaticClass.Encrypt(sourceString);

string decryptedString = SomeStaticClass.Decrypt(encryptedString);

しかし、saltやkey、byte[]などにまつわる面倒な作業は最小限にしています。

ググってみたけど、何を見つけても混乱する(これがまやかしの質問であることは、類似のSO Qsのリストを見てもらえばわかる)。

どのように解決するのですか?

2015年12月23日に更新しました。この回答は多くのupvoteを得ているようなので、コメントとフィードバックに基づいて、愚かなバグを修正し、一般的にコードを改善するために更新しました。 具体的な改善点については、記事の最後をご覧ください。

他の人も言っているように、暗号は単純ではないので、暗号化アルゴリズムを自分で開発することは避けた方がいいでしょう。

しかし、組み込みの RijndaelManaged 暗号クラス。

Rijndael は、現在の 高度な暗号化規格 ということで、確かに「ベストプラクティス」と言えるアルゴリズムを使っていますね。

RijndaelManaged クラスは通常、バイト配列、ソルト、キー、初期化ベクトルなどを扱う必要がありますが、これはまさにラッパー・クラスの中で抽象化することができる種類の詳細です。

文字列ベースの平文を文字列ベースのパスワードで暗号化し、暗号化された文字列を文字列として表現するためのシンプルなメソッドです。 もちろん、同じパスワードで暗号化された文字列を復号化する同等のメソッドも存在します。

このコードの最初のバージョンは、毎回全く同じソルトとIVの値を使用していましたが、この新しいバージョンは、毎回ランダムなソルトとIVの値を生成します。 ある文字列の暗号化と復号化では、ソルトとIVは同じでなければならないため、暗号化の際には暗号文の先頭にソルトとIVを付加し、復号化の際には再び暗号文からソルトとIVを抜き出す。 この結果、全く同じ平文を全く同じパスワードで暗号化すると、毎回全く異なる暗号文の結果が得られることになります。

これを利用する際の強みは RijndaelManaged クラスを使用して暗号化を行い、さらに Rfc2898DeriveBytes の関数を使用します。 System.Security.Cryptography 名前空間は、標準的で安全なアルゴリズムを使って暗号鍵を生成します (具体的には。 PBKDF2 を使用します。 (これは、最初のバージョンの古いPBKDF1アルゴリズムの使用を改善したものであることに注意してください)。

最後に、重要なのは、これはまだ 非認証 暗号化です。 暗号化だけではプライバシー(メッセージを第三者に知られない)しか提供できませんが、認証付き暗号化ではプライバシーと認証(メッセージが送信者から送られたことを受信者が知る)の両方を提供することを目的としています。

あなたの正確な要求を知らない限り、このコードがあなたのニーズに対して十分に安全であるかどうかを言うのは難しいですが、実装の相対的な単純さと品質との間の良いバランスを提供するために作成されています。 例えば、暗号化された文字列の受信者が、信頼できる送信者から直接文字列を受信している場合、認証は以下のようになります。 は必要ないかもしれません。 .

より複雑で、認証された暗号化を必要とする場合は、以下をご覧ください。 本論文 を実装しています。

以下はそのコードです。

using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Linq;

namespace EncryptStringSample
{
    public static class StringCipher
    {
        // This constant is used to determine the keysize of the encryption algorithm in bits.
        // We divide this by 8 within the code below to get the equivalent number of bytes.
        private const int Keysize = 256;

        // This constant determines the number of iterations for the password bytes generation function.
        private const int DerivationIterations = 1000;

        public static string Encrypt(string plainText, string passPhrase)
        {
            // Salt and IV is randomly generated each time, but is preprended to encrypted cipher text
            // so that the same Salt and IV values can be used when decrypting.  
            var saltStringBytes = Generate256BitsOfRandomEntropy();
            var ivStringBytes = Generate256BitsOfRandomEntropy();
            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write))
                            {
                                cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length);
                                cryptoStream.FlushFinalBlock();
                                // Create the final bytes as a concatenation of the random salt bytes, the random iv bytes and the cipher bytes.
                                var cipherTextBytes = saltStringBytes;
                                cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray();
                                cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray();
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Convert.ToBase64String(cipherTextBytes);
                            }
                        }
                    }
                }
            }
        }

        public static string Decrypt(string cipherText, string passPhrase)
        {
            // Get the complete stream of bytes that represent:
            // [32 bytes of Salt] + [32 bytes of IV] + [n bytes of CipherText]
            var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText);
            // Get the saltbytes by extracting the first 32 bytes from the supplied cipherText bytes.
            var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(Keysize / 8).ToArray();
            // Get the IV bytes by extracting the next 32 bytes from the supplied cipherText bytes.
            var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(Keysize / 8).Take(Keysize / 8).ToArray();
            // Get the actual cipher text bytes by removing the first 64 bytes from the cipherText string.
            var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip((Keysize / 8) * 2).Take(cipherTextBytesWithSaltAndIv.Length - ((Keysize / 8) * 2)).ToArray();

            using (var password = new Rfc2898DeriveBytes(passPhrase, saltStringBytes, DerivationIterations))
            {
                var keyBytes = password.GetBytes(Keysize / 8);
                using (var symmetricKey = new RijndaelManaged())
                {
                    symmetricKey.BlockSize = 256;
                    symmetricKey.Mode = CipherMode.CBC;
                    symmetricKey.Padding = PaddingMode.PKCS7;
                    using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes))
                    {
                        using (var memoryStream = new MemoryStream(cipherTextBytes))
                        {
                            using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read))
                            {
                                var plainTextBytes = new byte[cipherTextBytes.Length];
                                var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length);
                                memoryStream.Close();
                                cryptoStream.Close();
                                return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount);
                            }
                        }
                    }
                }
            }
        }

        private static byte[] Generate256BitsOfRandomEntropy()
        {
            var randomBytes = new byte[32]; // 32 Bytes will give us 256 bits.
            using (var rngCsp = new RNGCryptoServiceProvider())
            {
                // Fill the array with cryptographically secure random bytes.
                rngCsp.GetBytes(randomBytes);
            }
            return randomBytes;
        }
    }
}

上記のクラスは、以下のようなコードで簡単に使用することができます。

using System;

namespace EncryptStringSample
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Please enter a password to use:");
            string password = Console.ReadLine();
            Console.WriteLine("Please enter a string to encrypt:");
            string plaintext = Console.ReadLine();
            Console.WriteLine("");

            Console.WriteLine("Your encrypted string is:");
            string encryptedstring = StringCipher.Encrypt(plaintext, password);
            Console.WriteLine(encryptedstring);
            Console.WriteLine("");

            Console.WriteLine("Your decrypted string is:");
            string decryptedstring = StringCipher.Decrypt(encryptedstring, password);
            Console.WriteLine(decryptedstring);
            Console.WriteLine("");

            Console.WriteLine("Press any key to exit...");
            Console.ReadLine();
        }
    }
}

(簡単なVS2013のサンプルソリューション(いくつかのユニットテストを含む)をダウンロードすることができます) こちら ).

UPDATE 2015年12月23日 コードの具体的な改善点を列挙します。

  • 暗号化するときとするときとでエンコードが異なるという愚かなバグを修正した。 を復号化しました。 salt & IV値の生成の仕組みが変わったため、エンコーディングは不要になりました。
  • salt/IVの変更に伴い、16文字の文字列をUTF8でエンコードすると32バイトになると誤って表示していた以前のコードコメントは適用できなくなりました(エンコードが不要になったため)。
  • 古いPBKDF1アルゴリズムの使用は、より近代的なPBKDF2アルゴリズムの使用と置き換えられました。
  • パスワードの導出は、以前は全く塩漬けされていませんでしたが、現在は適切に塩漬けされています(別の愚かなバグが解消されました)。