I need to perform RSA decryption with public key. Following is my code and it's returning junk values after decryption.
Encryption code
public static string EncryptWithPrivate(byte[] bytes)
{
AsymmetricKeyParameter privatekey = null;
using (var reader = File.OpenText(Path.Combine(Startup.Root, $"Certificates\\private.pem")))
{
var keypair = new PemReader(reader).ReadObject() as AsymmetricCipherKeyPair;
privatekey = keypair.Private;
}
try
{
var engine = new Pkcs1Encoding(new RsaEngine());
engine.Init(true, privatekey);
var encryptedBytes = engine.ProcessBlock(bytes, 0, bytes.Length);
if (encryptedBytes.Length > 0)
{
var encryptedString = Convert.ToBase64String(encryptedBytes);
return encryptedString;
}
}
catch (Exception ex)
{
ex.Log();
return ex.Message;
}
return string.Empty;
}
Decryption Code
public static string DecryptWithPublic(byte[] bytes)
{
AsymmetricKeyParameter publicKey = null;
using (var privateKeyTextReader = new StringReader(File.ReadAllText(Path.Combine(Startup.Root, $"Certificates\\public.pem"))))
{
publicKey = (AsymmetricKeyParameter)new PemReader(privateKeyTextReader).ReadObject();
}
try
{
var decryptEngine = new Pkcs1Encoding(new RsaEngine());
decryptEngine.Init(false, publicKey);
return Encoding.UTF8.GetString(decryptEngine.ProcessBlock(bytes, 0, bytes.Length));
}
catch (Exception ex)
{
ex.Log();
}
return string.Empty;
}
Test string: "12345678"
Output after decryption:
What am I doing wrong here?
I have this fairly basic code which works as I'd expect, note this is only test code stuffed into one method:
public string DoStuff(string input, byte[] customModulus)
{
var _privateKeyXML = "";
var _publicKeyXml = "";
using (var rsaServer = new RSACryptoServiceProvider(2048))
{
//var privateKey = rsaServer.ExportParameters(true);
//privateKey.Modulus = customModulus;
//rsaServer.ImportParameters(privateKey);
_privateKeyXML = rsaServer.ToXmlString(true);
_publicKeyXml = rsaServer.ToXmlString(false);
}
var inputBytes = Encoding.UTF8.GetBytes(input);
byte[] encryptedBytes = null;
using (var rsaClient = new RSACryptoServiceProvider(2048))
{
rsaClient.FromXmlString(_publicKeyXml);
encryptedBytes = rsaClient.Encrypt(inputBytes, false);
}
using (var rsaClient = new RSACryptoServiceProvider(2048))
{
rsaClient.FromXmlString(_privateKeyXML);
inputBytes = rsaClient.Decrypt(encryptedBytes, false);
}
input = Encoding.UTF8.GetString(inputBytes);
return input;
}
But if I un-comment the lines which set my customModulus, the rsaClient.Decrypt method throws an exception: System.Security.Cryptography.CryptographicException: 'A device attached to the system is not functioning.'
My customModulus is a byte[256]
I am running below code to get public and private key only, but it seems it outputs the whole XML format. I only need to output the keys as shown in Public and Private Key demo
static RSACryptoServiceProvider rsa;
private RSAParameters _privateKey;
private RSAParameters _publicKey;
public RSACrypto()
{
rsa = new RSACryptoServiceProvider(2048);
_privateKey = rsa.ExportParameters(true);
_publicKey = rsa.ExportParameters(false);
}
public string GetPublicKeyString()
{
var sw = new StringWriter();
var xs = new XmlSerializer(typeof(RSAParameters));
xs.Serialize(sw, _publicKey);
return sw.ToString();
}
public string GetPrivateKeyString()
{
var sw = new StringWriter();
var xs = new XmlSerializer(typeof(RSAParameters));
xs.Serialize(sw, _privateKey);
return sw.ToString();
}
Starting in .NET Core 3.0, this is (largely) built-in.
Writing SubjectPublicKeyInfo and RSAPrivateKey
.NET Core 3.0 built-in API
The output of the builtin API is the binary representation, to make them PEM you need to output the header, footer, and base64:
private static string MakePem(byte[] ber, string header)
{
StringBuilder builder = new StringBuilder("-----BEGIN ");
builder.Append(header);
builder.AppendLine("-----");
string base64 = Convert.ToBase64String(ber);
int offset = 0;
const int LineLength = 64;
while (offset < base64.Length)
{
int lineEnd = Math.Min(offset + LineLength, base64.Length);
builder.AppendLine(base64.Substring(offset, lineEnd - offset));
offset = lineEnd;
}
builder.Append("-----END ");
builder.Append(header);
builder.AppendLine("-----");
return builder.ToString();
}
So to produce the strings:
string publicKey = MakePem(rsa.ExportSubjectPublicKeyInfo(), "PUBLIC KEY");
string privateKey = MakePem(rsa.ExportRSAPrivateKey(), "RSA PRIVATE KEY");
Semi-manually
If you can't use .NET Core 3.0, but you can use pre-release NuGet packages, you can make use of the prototype ASN.1 writer package (which is the same code that's used internally in .NET Core 3.0; it's just that the API surface isn't finalized).
To make the public key:
private static string ToSubjectPublicKeyInfo(RSA rsa)
{
RSAParameters rsaParameters = rsa.ExportParameters(false);
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
writer.PushSequence();
writer.PushSequence();
writer.WriteObjectIdentifier("1.2.840.113549.1.1.1");
writer.WriteNull();
writer.PopSequence();
AsnWriter innerWriter = new AsnWriter(AsnEncodingRules.DER);
innerWriter.PushSequence();
WriteRSAParameter(innerWriter, rsaParameters.Modulus);
WriteRSAParameter(innerWriter, rsaParameters.Exponent);
innerWriter.PopSequence();
writer.WriteBitString(innerWriter.Encode());
writer.PopSequence();
return MakePem(writer.Encode(), "PUBLIC KEY");
}
And to make the private key:
private static string ToRSAPrivateKey(RSA rsa)
{
RSAParameters rsaParameters = rsa.ExportParameters(true);
AsnWriter writer = new AsnWriter(AsnEncodingRules.DER);
writer.PushSequence();
writer.WriteInteger(0);
WriteRSAParameter(writer, rsaParameters.Modulus);
WriteRSAParameter(writer, rsaParameters.Exponent);
WriteRSAParameter(writer, rsaParameters.D);
WriteRSAParameter(writer, rsaParameters.P);
WriteRSAParameter(writer, rsaParameters.Q);
WriteRSAParameter(writer, rsaParameters.DP);
WriteRSAParameter(writer, rsaParameters.DQ);
WriteRSAParameter(writer, rsaParameters.InverseQ);
writer.PopSequence();
return MakePem(writer.Encode(), "RSA PRIVATE KEY");
}
Reading them back
.NET Core 3.0 built-in API
Except that .NET Core 3.0 doesn't understand PEM encoding, so you have to do PEM->binary yourself:
private const string RsaPrivateKey = "RSA PRIVATE KEY";
private const string SubjectPublicKeyInfo = "PUBLIC KEY";
private static byte[] PemToBer(string pem, string header)
{
// Technically these should include a newline at the end,
// and either newline-or-beginning-of-data at the beginning.
string begin = $"-----BEGIN {header}-----";
string end = $"-----END {header}-----";
int beginIdx = pem.IndexOf(begin);
int base64Start = beginIdx + begin.Length;
int endIdx = pem.IndexOf(end, base64Start);
return Convert.FromBase64String(pem.Substring(base64Start, endIdx - base64Start));
}
Once that's done you can now load the keys:
using (RSA rsa = RSA.Create())
{
rsa.ImportRSAPrivateKey(PemToBer(pemPrivateKey, RsaPrivateKey), out _);
...
}
using (RSA rsa = RSA.Create())
{
rsa.ImportSubjectPublicKeyInfo(PemToBer(pemPublicKey, SubjectPublicKeyInfo), out _);
...
}
Semi-manually
If you can't use .NET Core 3.0, but you can use pre-release NuGet packages, you can make use of the prototype ASN.1 reader package (which is the same code that's used internally in .NET Core 3.0; it's just that the API surface isn't finalized).
For the public key:
private static RSA FromSubjectPublicKeyInfo(string pem)
{
AsnReader reader = new AsnReader(PemToBer(pem, SubjectPublicKeyInfo), AsnEncodingRules.DER);
AsnReader spki = reader.ReadSequence();
reader.ThrowIfNotEmpty();
AsnReader algorithmId = spki.ReadSequence();
if (algorithmId.ReadObjectIdentifierAsString() != "1.2.840.113549.1.1.1")
{
throw new InvalidOperationException();
}
algorithmId.ReadNull();
algorithmId.ThrowIfNotEmpty();
AsnReader rsaPublicKey = spki.ReadSequence();
RSAParameters rsaParameters = new RSAParameters
{
Modulus = ReadNormalizedInteger(rsaPublicKey),
Exponent = ReadNormalizedInteger(rsaPublicKey),
};
rsaPublicKey.ThrowIfNotEmpty();
RSA rsa = RSA.Create();
rsa.ImportParameters(rsaParameters);
return rsa;
}
private static byte[] ReadNormalizedInteger(AsnReader reader)
{
ReadOnlyMemory<byte> memory = reader.ReadIntegerBytes();
ReadOnlySpan<byte> span = memory.Span;
if (span[0] == 0)
{
span = span.Slice(1);
}
return span.ToArray();
}
And because the private key values have to have the correct size arrays, the private key one is just a little trickier:
private static RSA FromRSAPrivateKey(string pem)
{
AsnReader reader = new AsnReader(PemToBer(pem, RsaPrivateKey), AsnEncodingRules.DER);
AsnReader rsaPrivateKey = reader.ReadSequence();
reader.ThrowIfNotEmpty();
if (!rsaPrivateKey.TryReadInt32(out int version) || version != 0)
{
throw new InvalidOperationException();
}
byte[] modulus = ReadNormalizedInteger(rsaPrivateKey);
int halfModulusLen = (modulus.Length + 1) / 2;
RSAParameters rsaParameters = new RSAParameters
{
Modulus = modulus,
Exponent = ReadNormalizedInteger(rsaPrivateKey),
D = ReadNormalizedInteger(rsaPrivateKey, modulus.Length),
P = ReadNormalizedInteger(rsaPrivateKey, halfModulusLen),
Q = ReadNormalizedInteger(rsaPrivateKey, halfModulusLen),
DP = ReadNormalizedInteger(rsaPrivateKey, halfModulusLen),
DQ = ReadNormalizedInteger(rsaPrivateKey, halfModulusLen),
InverseQ = ReadNormalizedInteger(rsaPrivateKey, halfModulusLen),
};
rsaPrivateKey.ThrowIfNotEmpty();
RSA rsa = RSA.Create();
rsa.ImportParameters(rsaParameters);
return rsa;
}
private static byte[] ReadNormalizedInteger(AsnReader reader, int length)
{
ReadOnlyMemory<byte> memory = reader.ReadIntegerBytes();
ReadOnlySpan<byte> span = memory.Span;
if (span[0] == 0)
{
span = span.Slice(1);
}
byte[] buf = new byte[length];
int skipSize = length - span.Length;
span.CopyTo(buf.AsSpan(skipSize));
return buf;
}
The Bouncycastle C# library has some helper classes that can make this relatively easy. It is not well documented unfortunately. Here is an example:
using System;
using System.IO;
using System.Security.Cryptography;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
namespace ExportToStandardFormats
{
class MainClass
{
public static void Main(string[] args)
{
var rsa = new RSACryptoServiceProvider(2048);
var rsaKeyPair = DotNetUtilities.GetRsaKeyPair(rsa);
var writer = new StringWriter();
var pemWriter = new PemWriter(writer);
pemWriter.WriteObject(rsaKeyPair.Public);
pemWriter.WriteObject(rsaKeyPair.Private);
Console.WriteLine(writer);
}
}
}
I wanted to extract public and private key as char array, and not as string. I found one solution which is a modification of answers provided by James and Vikram above. It could be helpful for someone looking for it.
public static void GenerateKeyPair()
{
char[] private_key= null;
char[] public_key=null;
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048);
var rsaKeyPair = DotNetUtilities.GetRsaKeyPair(rsa);
//PrivateKey
MemoryStream memoryStream = new MemoryStream();
TextWriter streamWriter = new StreamWriter(memoryStream);
PemWriter pemWriter = new PemWriter(streamWriter);
pemWriter.WriteObject(rsaKeyPair.Private);
streamWriter.Flush();
byte[] bytearray = memoryStream.GetBuffer();
private_key = Encoding.ASCII.GetChars(bytearray);
//PublicKey
memoryStream = new MemoryStream();
streamWriter = new StreamWriter(memoryStream);
pemWriter = new PemWriter(streamWriter);
pemWriter.WriteObject(rsaKeyPair.Public);
streamWriter.Flush();
bytearray = memoryStream.GetBuffer();
public_key = Encoding.ASCII.GetChars(bytearray);
}
Adding on to the above answer, to get public and private key separately in string format, you can use the following code snippet.
public static void GenerateKeyPair()
{
try
{
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(2048);
var rsaKeyPair = DotNetUtilities.GetRsaKeyPair(rsa);
//Getting publickey
TextWriter textWriter = new StringWriter();
PemWriter pemWriter = new PemWriter(textWriter);
pemWriter.WriteObject(rsaKeyPair.Public);
publicKey = textWriter.ToString();
//Getting privatekey
textWriter = new StringWriter();
pemWriter = new PemWriter(textWriter);
pemWriter.WriteObject(rsaKeyPair.Private);
privateKey = textWriter.ToString();
Console.WriteLine("public key, {0}", publicKey);
Console.WriteLine("private key, {0}", privateKey);
}
catch (Exception e)
{
Console.WriteLine($"GenerateKeyPair Failed with {e}");
Console.WriteLine(e);
}
}
When I use ArmoredOutputStream I get a "-----BEGIN PGP MESSAGE-----" instead of a "-----BEGIN PGP SIGNATURE-----" after the clear text. I cannot figure out how to use ArmoredOutputStream on my own. Sometimes I get the signature header, but have non-base64 characters in the message. The BC legionaries must be sadists releasing such a good library with nearly no documentation how to use it...
Here is my code:
private void doTestSig(
PublicKeyAlgorithmTag encAlgorithm,
HashAlgorithmTag hashAlgorithm,
PgpPublicKey pubKey,
PgpPrivateKey privKey)
{
MemoryStream testIn = new MemoryStream(TEST_DATA, false);
MemoryStream baseOut = new MemoryStream();
ArmoredOutputStream aOut = new ArmoredOutputStream(baseOut);
aOut.BeginClearText(hashAlgorithm);
aOut.Write(testIn.ToArray(), 0, testIn.ToArray().Length);
aOut.EndClearText();
PgpSignatureGenerator sGen = new PgpSignatureGenerator(encAlgorithm, hashAlgorithm);
sGen.InitSign(PgpSignature.BinaryDocument, privKey);
sGen.GenerateOnePassVersion(false).Encode(aOut);
PgpLiteralDataGenerator lGen = new PgpLiteralDataGenerator();
Stream lOut = lGen.Open(
new FilterStream(aOut),
PgpLiteralData.Binary,
PgpLiteralDataGenerator.Console,
TEST_DATA.Length * 2,
DateTime.UtcNow);
int ch;
while ((ch = testIn.ReadByte()) >= 0)
{
aOut.WriteByte((byte)ch);
sGen.Update((byte)ch);
}
lOut.Write(TEST_DATA, 0, TEST_DATA.Length);
sGen.Update(TEST_DATA);
lGen.Close();
sGen.Generate().Encode(aOut);
aOut.Close();
byte[] outarr = baseOut.ToArray();
string strOut = System.Text.Encoding.UTF8.GetString(outarr, 0, outarr.Length);
// strOut gets this:
// "-----BEGIN PGP SIGNED MESSAGE-----\r\n
// Hash: SHA1\r\n
// \r\n
// hello world!\n
// hello world!\n
//
// -----BEGIN PGP MESSAGE-----\r\n
// Version: BCPG C# v1.7.0.0
// \r\n
// \r\n
// kA0DAAIR4GIXExqVFDEBy0JiCF9DT05TT0xFVBcoRmhlbGxvIHdvcmxkIQpoZWxs\r\n
// byB3b3JsZCEKaGVsbG8gd29ybGQhCmhlbGxvIHdvcmxkIQqIRgQAEQIABgUCVBco\r\n
// RgAKCRDgYhcTGpUUMXglAJwNOiEzA7H9RxaoU/gs9AoxZCOd+ACffl8lJ/6ZNpkB\r\n
// FLBjbd9wlEU3/SY=\r\n=u7Mk\r\n
// -----END PGP MESSAGE-----\r\n"
}
Finally I found a source code that demonstrates the use of ArmoredOutputStream:
// Setup signature stuff
var signatureData = new PgpSignatureGenerator(encAlgorithm, hashAlgorithm);
signatureData.InitSign(PgpSignature.CanonicalTextDocument, privKey);
foreach (string userId in pubKey.GetUserIds())
{
var subPacketGenerator = new PgpSignatureSubpacketGenerator();
subPacketGenerator.SetSignerUserId(false, userId);
signatureData.SetHashedSubpackets(subPacketGenerator.Generate());
// Just the first one!
break;
}
using (var sout = new MemoryStream())
{
using (var armoredOut = new ArmoredOutputStream(sout))
{
armoredOut.BeginClearText(hashAlgorithm);
using (MemoryStream testIn = new MemoryStream(TEST_DATA, false))
{
int ch;
while ((ch = testIn.ReadByte()) >= 0)
{
armoredOut.WriteByte((byte)ch);
signatureData.Update((byte)ch);
}
}
armoredOut.EndClearText();
using (var outputStream = new BcpgOutputStream(armoredOut))
{
signatureData.Generate().Encode(outputStream);
}
byte[] outarr = sout.ToArray();
string strOut = System.Text.Encoding.UTF8.GetString(outarr, 0, outarr.Length);
Note that this code doesn't include a fix for the missing linebreak before '-----BEGIN PGP SIGNATURE-----' bug in the library. You have to add it yourself in the output.
We have an legacy web application which is using Persits Software's aspEncrypt. I have thousands of files encrypted with that setup so I need to stay compatible.
I am trying to write a new utility application in C# to match that same encryption. The goal here is to NOT use aspEncrypt in the new application and to match the encryption using the stuff built into the .NET framework. If successful then I could use the same legacy decryption routines on both the legacy and new files.
Sample code for how I handle the encryption step using aspEncrypt:
public static int EncryptFileWithRawKey2()
{
string InputFile = #"D:\TEMP\004\testfile.txt"; // contains single line of ANSI text with no end of line characters "hello this is a file"
string OutPutFile = #"D:\TEMP\004\testfile.txt.3.xxx";
string RawKey = "4CDD1518CD6662C9D1583A8732173EDC";
int iRet = 0;
ASPENCRYPTLib.ICryptoKey eKey = null;
try
{
ASPENCRYPTLib.CryptoManager CM = new ASPENCRYPTLib.CryptoManager();
ASPENCRYPTLib.ICryptoContext Context = CM.OpenContext("", 0, 0);
ASPENCRYPTLib.ICryptoBlob Blob = CM.CreateBlob();
Blob.Hex = RawKey;
eKey = Context.ImportRawKey(Blob, ASPENCRYPTLib.CryptoAlgorithms.calgRC2);
ASPENCRYPTLib.ICryptoBlob IVblob = CM.CreateBlob();
IVblob.Hex = "0000000000000000";
eKey.SetIV(IVblob);
eKey.Mode = ASPENCRYPTLib.CryptoCipherModes.ccmCBC;
eKey.Padding = ASPENCRYPTLib.CryptoCipherPadding.ccpPKCS5;
eKey.EffectiveLength = 40;
eKey.EncryptFile(InputFile.Trim(), OutPutFile.Trim());
if (File.Exists(OutPutFile) == true)
iRet = 1;
}
catch (Exception e)
{
Console.WriteLine("EncryptFileWithRawKey2 FAILED");
Console.WriteLine(e.ToString());
iRet = - 1;
}
return iRet;
}
And here is the code I wrote in what has so far been an unsuccessful attempt to match it:
public static void EncryptTextToFile2()
{
String Data = "hello this is a file";
String FileName = #"D:\TEMP\004\testfile.txt.4.xxx";
String sTempKey = "4CDD1518CD6662C9D1583A8732173EDC";
byte[] Key;
byte[] IV;
try
{
Key = StringToByteArray(sTempKey);
IV = StringToByteArray("0000000000000000");
FileStream fStream = File.Open(FileName, FileMode.OpenOrCreate);
RC2 RC2alg = RC2.Create();
RC2alg.Padding = PaddingMode.PKCS7;
RC2alg.Mode = CipherMode.CBC;
RC2alg.KeySize = 40;
RC2alg.EffectiveKeySize = 40;
CryptoStream cStream = new CryptoStream(fStream, RC2alg.CreateEncryptor(Key, IV), CryptoStreamMode.Write);
cStream.FlushFinalBlock();
StreamWriter sWriter = new StreamWriter(cStream);
sWriter.WriteLine(Data);
sWriter.Close();
cStream.Close();
fStream.Close();
}
catch (CryptographicException e)
{
Console.WriteLine("A Cryptographic error occurred: {0}", e.Message);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine("A file error occurred: {0}", e.Message);
}
}
public static byte[] StringToByteArray(string hex)
{
return Enumerable.Range(0, hex.Length)
.Where(x => x % 2 == 0)
.Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
.ToArray();
}
The output from the two routines does not match. I cannot change the logic which uses the AspEncrypt libraries so I need to change the second section of code in some way to match the output from the first.