C# - Rabin-Miller 소수 생성 방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 - 두 번째 이야기
예전에 올렸던 소스 코드를,
C# - Rabin-Miller 소수 생성 방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자
; https://www.sysnet.pe.kr/2/0/1300
개선해 보았습니다. 일단, 저 글에 공개된 소스 코드의 Main을 무한 루프로 고치고 실행하면,
static void Main(string[] args)
{
while (true)
{
FillRSA(5);
}
}
예외가 발생할 수 있는데, 이는 이미
1편에서 글을 쓴데로 P 값보다 Q 값이 더 큰 경우에 해당합니다. 제가 웬일인지 그 부분을 주석처리했는데, 다음과 같이 주석을 해제해 주시면 됩니다.
if (q > p)
{
BigInteger temp = q;
q = p;
p = temp;
}
그런데, 이렇게 해도 여전히 다음과 같은 예외가 발생합니다.
byte[] dataToEncrypt = ByteConverter.GetBytes("Data to Encrypt");
byte[] encryptedData;
byte[] decryptedData;
encryptedData = RSAEncrypt(dataToEncrypt, rsa.ExportParameters(false), false);
decryptedData = RSADecrypt(encryptedData, rsa.ExportParameters(true), false);
Console.WriteLine("Decrypted plaintext: {0}", ByteConverter.GetString(decryptedData));
System.ArgumentNullException was unhandled
HResult=-2147467261
Message=Array cannot be null.
Parameter name: bytes
ParamName=bytes
Source=mscorlib
StackTrace:
at System.Text.Encoding.GetString(Byte[] bytes)
at producePrime.Program.FillRSA(Int32 certainty) in E:\...\producePrime\Program.cs:line 159
at producePrime.Program.Main(String[] args) in E:\...\producePrime\Program.cs:line 16
InnerException:
결과는 null이지만, RSADecrypt에서 예외가 발생했기 때문에 decryptedData에 null이 들어간 것입니다. 결국 최초의 예외는 다음에서 발생합니다.
System.Security.Cryptography.CryptographicException: The parameter is incorrect.
at System.Security.Cryptography.CryptographicException.ThrowCryptographicException(Int32 hr)
at System.Security.Cryptography.RSACryptoServiceProvider.DecryptKey(SafeKeyHandle pKeyContext, Byte[] pbEncryptedKey,
Int32 cbEncryptedKey, Boolean fOAEP, ObjectHandleOnStack ohRetDecryptedKey)
at System.Security.Cryptography.RSACryptoServiceProvider.Decrypt(Byte[] rgb, Boolean fOAEP)
at producePrime.Program.RSADecrypt(Byte[] DataToDecrypt, RSAParameters RSAKeyInfo, Boolean DoOAEPPadding) in E:\...\producePrime\Program.cs:line 309
이에 대한 원인이 재미있는데요. RSAParameters.Modulus의 첫 번째 바이트에 0이 들어갔기 때문입니다. 즉, P * Q의 값이 충분히 크지 않게 되면 발생하는 것인데, 가령 P와 Q의 값이 각각 64바이트 길이인데, 그 곱하기 값인 Modulus가 128바이트를 모두 채우지 않는 경우가 되면 (암호화 메서드인 Encrypt 호출은 통과하지만) 복호화 메서드인 Decrypt 호출 시 "The parameter is incorrect" 예외가 발생하는 것입니다.
따라서, 이런 조건을 갖는 P, Q의 키 값이 생성된 경우에는 그냥 버리고 다시 P, Q를 생성해야 합니다. 이렇게 해서 안정적인 버전의 소스 코드를 만들면 다음과 같습니다. ^^
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.Numerics;
using System.Diagnostics;
namespace producePrime
{
class Program
{
static void Main(string[] args)
{
while (true)
{
FillRSA(5);
}
}
static void FillRSA(int certainty)
{
int modulusSize = 1024;
int modulusByteSize = modulusSize / 8 / 2;
RSAParameters rsaParam = new RSAParameters();
// P, Q 값 생성하고,
BigInteger p, q;
Stopwatch st = new Stopwatch();
st.Start();
ProducePQ(modulusByteSize, out p, certainty);
ProducePQ(modulusByteSize, out q, certainty);
st.Stop();
Console.WriteLine("Elapsed: " + st.ElapsedMilliseconds + "(ms)");
if (q > p)
{
BigInteger temp = q;
q = p;
p = temp;
}
// N 값 구하고,
BigInteger n = p * q;
// E 값 생성하고,
int publicExponent = 65537;
// Φ 값 구하고,
BigInteger rp = (p - 1) * (q - 1);
// D 값 구하고,
BigInteger d = GetExtendedGcd(publicExponent, rp).Item2;
if (d < 0)
{
BigInteger tMultiplier = BigInteger.Min(publicExponent, rp);
BigInteger tDivisor = BigInteger.Max(publicExponent, rp);
d = tDivisor + d;
Console.WriteLine("d < 0");
Console.WriteLine();
BigInteger dMod = BigInteger.Remainder(d * tMultiplier, tDivisor);
if (dMod.IsOne == false)
{
Console.WriteLine("[Failed] d * e mod rp == " + dMod);
Console.WriteLine();
return;
}
}
// InverseQ 값 구하고,
BigInteger inverseQ = GetExtendedGcd(q, p).Item2;
if (inverseQ < 0)
{
BigInteger tMultiplier = BigInteger.Min(q, p);
BigInteger tDivisor = BigInteger.Max(q, p);
inverseQ = tDivisor + inverseQ;
Console.WriteLine("inverseQ < 0");
Console.WriteLine();
BigInteger qMod = BigInteger.Remainder(inverseQ * tMultiplier, tDivisor);
if (qMod.IsOne == false)
{
Console.WriteLine("[Failed] inverseQ * q mod p == " + qMod);
Console.WriteLine();
return;
}
}
// DP, DQ 값 구하고,
BigInteger dp = BigInteger.Remainder(d, (p - 1));
BigInteger dq = BigInteger.Remainder(d, (q - 1));
ToBigEndian(p, ref rsaParam.P, modulusByteSize);
ToBigEndian(q, ref rsaParam.Q, modulusByteSize);
ToBigEndian(n, ref rsaParam.Modulus, modulusByteSize * 2);
if (rsaParam.Modulus[0] == 0)
{
return;
}
ToBigEndian(publicExponent, ref rsaParam.Exponent, 3);
ToBigEndian(d, ref rsaParam.D, modulusByteSize * 2);
ToBigEndian(dp, ref rsaParam.DP, modulusByteSize);
ToBigEndian(dq, ref rsaParam.DQ, modulusByteSize);
ToBigEndian(inverseQ, ref rsaParam.InverseQ, modulusByteSize);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParam);
// RSACryptoServiceProvider로 암/복호화 테스트
UnicodeEncoding ByteConverter = new UnicodeEncoding();
byte[] dataToEncrypt = ByteConverter.GetBytes("Data to Encrypt");
byte[] encryptedData;
byte[] decryptedData;
encryptedData = RSAEncrypt(dataToEncrypt, rsa.ExportParameters(false), false);
decryptedData = RSADecrypt(encryptedData, rsa.ExportParameters(true), false);
Console.WriteLine("Decrypted plaintext: {0}", ByteConverter.GetString(decryptedData));
}
private static void ToBigEndian(BigInteger bytes, ref byte[] target, int byteSize)
{
List<byte> list = new List<byte>();
list.AddRange(bytes.ToByteArray());
for (int i = list.Count; i < byteSize; i ++)
{
list.Add(0);
}
list.Reverse();
target = new byte[list.Count];
Array.Copy(list.ToArray(), target, list.Count);
}
private static Tuple<BigInteger, BigInteger> GetExtendedGcd(BigInteger num1, BigInteger num2)
{
if (num2 > num1)
{
return GetExtendedGcd(num2, num1);
}
BigInteger x = 0;
BigInteger lastx = 1;
BigInteger y = 1;
BigInteger lasty = 0;
BigInteger quotient = 0;
BigInteger tempNum2, tempx, tempy;
while (num2 != 0)
{
quotient = num1 / num2;
tempNum2 = num2;
num2 = num1 % num2;
num1 = tempNum2;
tempx = lastx - quotient * x;
lastx = x;
x = tempx;
tempy = lasty - quotient * y;
lasty = y;
y = tempy;
}
return new Tuple<BigInteger, BigInteger>(lastx, lasty);
}
private static void ProducePQ(int bytes, out BigInteger p, int certainty)
{
Random rand = new Random();
byte[] pRand = new byte[bytes];
while (true)
{
rand.NextBytes(pRand);
p = new BigInteger(pRand);
if (p.IsProbablePrime(certainty) == true)
{
break;
}
}
}
public static bool IsPrime(BigInteger candidate)
{
if ((candidate & 1) == 0)
{
if (candidate == 2)
{
return true;
}
else
{
return false;
}
}
for (BigInteger i = 3; (i * i) <= candidate; i += 2)
{
if ((candidate % i) == 0)
{
return false;
}
}
return candidate != 1;
}
static public byte[] RSAEncrypt(byte[] DataToEncrypt, RSAParameters RSAKeyInfo, bool DoOAEPPadding)
{
try
{
byte[] encryptedData;
using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
{
RSA.ImportParameters(RSAKeyInfo);
encryptedData = RSA.Encrypt(DataToEncrypt, DoOAEPPadding);
}
return encryptedData;
}
catch (CryptographicException e)
{
Console.WriteLine(e.Message);
return null;
}
}
static public byte[] RSADecrypt(byte[] DataToDecrypt, RSAParameters RSAKeyInfo, bool DoOAEPPadding)
{
try
{
byte[] decryptedData;
using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
{
RSA.ImportParameters(RSAKeyInfo);
decryptedData = RSA.Decrypt(DataToDecrypt, DoOAEPPadding);
}
return decryptedData;
}
catch (CryptographicException e)
{
Console.WriteLine(e.ToString());
return null;
}
}
}
}
(
첨부된 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]