C# で文字列を暗号化および復号化する [重複] 質問する

C# で文字列を暗号化および復号化する [重複] 質問する

C# で次の条件を満たす最も最新 (最良) の方法は何ですか?

string encryptedString = SomeStaticClass.Encrypt(sourceString);

string decryptedString = SomeStaticClass.Decrypt(encryptedString);

ただし、ソルト、キー、byte[] の操作などに伴う煩わしさは最小限です。

Google で検索しましたが、見つけた内容に困惑しています (同様の SO Q のリストを見ると、これが誤解を招く質問であることがわかります)。

ベストアンサー1

2015 年 12 月 23 日更新: この回答は多くの賛成票を獲得しているようなので、コメントやフィードバックに基づいて、些細なバグを修正し、コードを全体的に改善するために更新しました。具体的な改善点のリストについては、投稿の最後を参照してください。

他の人が言っているように、暗号化は単純ではないので、「独自の」暗号化アルゴリズムを作成することは避けるのが最善です。

しかし、組み込みのもののような独自のラッパークラスを作成することもできます。RijndaelManaged暗号化クラス。

ラインダールは現在のアルゴリズム名である高度暗号化標準つまり、確かに「ベストプラクティス」とみなせるアルゴリズムを使用していることになります。

RijndaelManagedクラスでは、バイト配列、ソルト、キー、初期化ベクトルなどを「いじくり回す」必要がありますが、これはまさに「ラッパー」クラス内である程度抽象化できる詳細です。

次のクラスは、まさにあなたが求めている種類のことを実行するために私がしばらく前に書いたものです。単純な単一のメソッド呼び出しにより、文字列ベースのプレーンテキストを文字列ベースのパスワードで暗号化し、結果として得られる暗号化された文字列も文字列として表すことができます。もちろん、同じパスワードで暗号化された文字列を復号化する同等のメソッドもあります。

毎回まったく同じ salt 値と IV 値を使用していたこのコードの最初のバージョンとは異なり、この新しいバージョンでは毎回ランダムな salt 値と IV 値が生成されます。特定の文字列の暗号化と復号化では salt 値と IV 値が同じでなければならないため、暗号化時に salt 値と IV が暗号文の先頭に追加され、復号化を実行するために再度暗号文から抽出されます。この結果、まったく同じパスワードでまったく同じ平文を暗号化すると、毎回まったく異なる暗号文が生成されます。

これを使うことの「強み」は、RijndaelManagedクラスを使用して暗号化を実行し、Rfc2898 派生バイト名前空間の機能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))
                            using (var streamReader = new StreamReader(cryptoStream, Encoding.UTF8))
                            {
                                return streamReader.ReadToEnd();
                            }
                        }
                    }
                }
            }
        }

        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サンプルソリューションをダウンロードできます)ここ)。

2015 年 12 月 23 日更新:コードに対する具体的な改善点のリストは次のとおりです。

  • 暗号化と復号化でエンコードが異なるというおかしなバグを修正しました。ソルトと IV 値が生成されるメカニズムが変更されたため、エンコードは不要になりました。
  • salt/IV の変更により、16 文字の文字列を UTF8 でエンコードすると 32 バイトが生成されるという誤った以前のコード コメントは適用されなくなりました (エンコードが不要になったため)。
  • 廃止された PBKDF1 アルゴリズムの使用は、より新しい PBKDF2 アルゴリズムの使用に置き換えられました。
  • パスワード導出は、以前はまったくソルト化されていませんでしたが、現在は適切にソルト化されています (別のばかげたバグが修正されました)。

おすすめ記事