RSAのEncrypt/Decrypt で例外が出るときの対処

要約

RSACryptoServiceProvider を使って Encrypt を呼び出した場合、文字列が長いと WindowsCryptographicException が出る。(Decryptの場合は CryptographicException) その場合は KeySize と パディングから最大長を割り出してそのサイズ以下でブロック化し暗号化・復号化を行う必要がある。また、暗号化したブロックのサイズは復号化するときにわかるように情報を付与しておかないと復号化は難しそうだ。

基本的には非対称暗号で大きなデータを扱うのではなく、共通鍵暗号をデータの暗号化に使用し、共通鍵の暗号化にのみ非対称暗号を使用するのが良いようだ。

参考資料

RSACryptoServiceProvider.Encrypt メソッド (System.Security.Cryptography) | Microsoft Docs

【C#】文字列を指定した文字数で分割する拡張メソッド - コガネブログ

Convert.ToBase64String メソッド (System) | Microsoft Docs

PHPのmcrypt関数で使用する初期化ベクトル(IV)とは公開されて… - 人力検索はてな

コード(非対称暗号での暗号化)

MSTest を使用して暗号化・復号化のサンプルを作成した。 TestEncryptAndDecrypt3 が今回のキモの部分。今回はパディングを PKCS # 1 v 1.5 で行うため、rsa.KeySize bit / 8 bit - 11 バイト = 117 バイトがブロック最大長となる。今回は暗号化したデータブロックを復号化サイドで判断できるようにするため、BASE64エンコードし改行で区切ることでブロックを分割した。

using System;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace EncryptTest
{
    [TestClass]
    public class UnitTest1
    {
        private string publicKey, privateKey;

        [TestInitialize]
        public void TestInit()
        {
            using (var rsa = new RSACryptoServiceProvider())
            {
                publicKey = rsa.ToXmlString(false);
                privateKey = rsa.ToXmlString(true);
            }
        }

        [DataTestMethod]
        [DataRow("Hello World!!")]
        [DataRow("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
        public void TestEncryptAndDecrypt1(string rawString)
        {
            var rawBytes = Encoding.ASCII.GetBytes(rawString);

            byte[] encryptedBytes;
            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(publicKey);
                encryptedBytes = rsa.Encrypt(rawBytes, false);
            }

            string decryptedString;
            using (var rsa = new RSACryptoServiceProvider())
            {
                byte[] decryptedBytes;

                rsa.FromXmlString(privateKey);
                decryptedBytes = rsa.Decrypt(encryptedBytes, false);
                decryptedString = Encoding.ASCII.GetString(decryptedBytes);
            }

            Assert.AreEqual(rawString, decryptedString);
        }

        [TestMethod]
        public void TestEncryptAndDecrypt2()
        {
            // 128 - 11 バイト (117 バイト) を超えると rsa.Encrypt() で例外が発生する。
            var rawString =
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

            var rawBytes = Encoding.ASCII.GetBytes(rawString);

            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(publicKey);
                try
                {
                    rsa.Encrypt(rawBytes, false);
                }
                catch (Exception e)
                {
                    Assert.AreEqual("WindowsCryptographicException", e.GetType().Name);
                    return;
                }
                Assert.Fail("This Test should be thrown exception.");
            }
        }

        [DataTestMethod]
        [DataRow("Hello World!!")]
        [DataRow("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
        [DataRow("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
        public void TestEncryptAndDecrypt3(string rawString)
        {
            string encryptedBase64String;
            using (var rsa = new RSACryptoServiceProvider())
            {
                rsa.FromXmlString(publicKey);

                var count = (rsa.KeySize / 8) - 11;
                var splitStrings = new List<string>();
                var length = (int)Math.Ceiling((double)rawString.Length / count);
                for (int i = 0; i < length; i++)
                {
                    int start = count * i;
                    if (rawString.Length <= start)
                    {
                        break;
                    }
                    if (rawString.Length < start + count)
                    {
                        splitStrings.Add(rawString.Substring(start));
                    }
                    else
                    {
                        splitStrings.Add(rawString.Substring(start, count));
                    }
                }

                var sb = new StringBuilder();
                foreach (var line in splitStrings)
                {
                    var tempBytes = Encoding.ASCII.GetBytes(line);
                    var tempEncBytes = rsa.Encrypt(tempBytes, false);
                    sb.AppendLine(Convert.ToBase64String(tempEncBytes, 0, tempEncBytes.Length));
                }
                encryptedBase64String = sb.ToString();
            }

            var decryptedString = "";
            using (var rsa = new RSACryptoServiceProvider())
            {
                byte[] decryptedBytes;

                rsa.FromXmlString(privateKey);

                foreach(var line in encryptedBase64String.Split('\n'))
                {
                    if (string.IsNullOrEmpty(line))
                        continue;

                    var base64Bytes = Convert.FromBase64String(line);
                    decryptedBytes = rsa.Decrypt(base64Bytes, false);
                    decryptedString += Encoding.ASCII.GetString(decryptedBytes);
                }
            }

            Assert.AreEqual(rawString, decryptedString);
        }
    }
}

コード(共通鍵暗号での暗号化)

さっきの例ではデータの暗号化に非対称暗号(RSA)を用いた。ただ、データが大きくなるにつれパフォーマンスの問題が出てくるらしい、なので、データの暗号化は共通鍵暗号を用い、共通鍵の暗号に非対称暗号を用いることで安全性・パフォーマンスの両面からカバーする。という方法が今の主流らしい。簡単に手順を示すと以下のようになる。

  1. 共通鍵受け渡しフロー

    1. 片側(A)で公開鍵・秘密鍵を生成
    2. (A)は公開鍵をもう一方(B)に受け渡す
    3. (B)は公開鍵を用い共通鍵を暗号化
    4. (B)は暗号化された共通鍵を(A)に渡す
    5. (A)は暗号化された共通鍵を自身の秘密鍵で復号化し保持
  2. 暗号化データ受け渡しフロー

    1. (B)は共通鍵でデータを暗号化
    2. (B)は暗号化されたデータをもう一方に受け渡す
    3. (A)は共通鍵で暗号化されたデータを復号化する

上記では便宜上データの暗号化を(B)で行っているが、共通鍵の受け渡しが終われば、データの暗号化はどちらが行っても問題なく行える。

以下、上記を実装したコードサンプルとなる。コード中の共通鍵のIVを平文で受け渡ししているが、暗号化・復号化が一回限りでIVを受け渡してすぐに行われるのであれば平文でもさほど問題ないようだ。(PHPのmcrypt関数で使用する初期化ベクトル(IV)とは公開されて… - 人力検索はてな)

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.Security.Cryptography;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AESTest
{
    [TestClass]
    public class UnitTest1
    {
        [DataTestMethod]
        [DataRow("Hello World!!")]
        [DataRow("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
        [DataRow("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
        public void TestEncryptAndDecrypt1(string rawString)
        {
            var decryptor = new Decryptor();
            var encryptor = new Encryptor();

            TransferCommonKey(encryptor, decryptor);

            byte[] iv, encryptedText;
            (iv, encryptedText) = encryptor.Encrypt(rawString);

            string decString;
            decString = decryptor.Decrypt(iv, encryptedText);

            Assert.AreEqual(rawString, decString);
        }

        private void TransferCommonKey(Encryptor encryptor, Decryptor decryptor)
        {
            string publicKey = decryptor.GetPublicKey();

            byte[] encryptedCommonKey = encryptor.GenerateEncryptedCommonKey(publicKey);

            decryptor.TransferCommonKey(encryptedCommonKey);
        }
    }

    class Encryptor
    {
        byte[] commonKey;

        public byte[] GenerateEncryptedCommonKey(string publicKey)
        {
            var aes = Aes.Create();
            commonKey = aes.Key;
            using var rsa = new RSACryptoServiceProvider();
            rsa.FromXmlString(publicKey);
            return rsa.Encrypt(aes.Key, false);
        }

        public (byte[], byte[]) Encrypt(string text)
        {
            byte[] encrypted;

            var aes = Aes.Create();
            using (var ms = new MemoryStream())
            using (var cryptStream = new CryptoStream(ms, aes.CreateEncryptor(commonKey, aes.IV), CryptoStreamMode.Write))
            {
                using (var sWriter = new StreamWriter(cryptStream))
                {
                    sWriter.Write(text);
                }
                encrypted = ms.ToArray();
            }

            return (aes.IV, encrypted);
        }
    }

    class Decryptor
    {
        string publicKey, privateKey;

        byte[] commonKey;

        public Decryptor()
        {
            using var rsa = new RSACryptoServiceProvider();
            publicKey = rsa.ToXmlString(false);
            privateKey = rsa.ToXmlString(true);
        }

        public string GetPublicKey()
        {
            return publicKey;
        }

        public void TransferCommonKey(byte[] encryptedCommonKey)
        {
            using var rsa = new RSACryptoServiceProvider();
            rsa.FromXmlString(privateKey);
            commonKey = rsa.Decrypt(encryptedCommonKey, false);
        }

        public string Decrypt(byte[] iv, byte[] data)
        {
            var aes = Aes.Create();
            using var ms = new MemoryStream(data);
            using var cryptStream = new CryptoStream(ms, aes.CreateDecryptor(commonKey, iv), CryptoStreamMode.Read);
            using var sReader = new StreamReader(cryptStream);
            return sReader.ReadLine();
        }
    }
}