I want to encrypt data in a web browser that is send to my C# backend and decrypted there.
That fails because I am unable to decrypt the data generated on the frontend in the backend.
Here's what I did so far.
First I created a private/public key pair (in XmlString Format). I took the ExportPublicKey function to generate the public key file from here: https://stackoverflow.com/a/28407693/98491
private static void GeneratePrivatePublicKeyPair() {
var name = "test";
var privateKeyXmlFile = name + "_priv.xml";
var publicKeyXmlFile = name + "_pub.xml";
var publicKeyFile = name + ".pub";
using var provider = new RSACryptoServiceProvider(1024);
File.WriteAllText(privateKeyXmlFile, provider.ToXmlString(true));
File.WriteAllText(publicKeyXmlFile, provider.ToXmlString(false));
using var publicKeyWriter = File.CreateText(publicKeyFile);
ExportPublicKey(provider, publicKeyWriter);
}
Now I can use the public key to encrypt data in my frontend.
(() => {
const publicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGy8btrbnSNPz7vWKfQXKxKXzg
28ZD8jCAd7gGYfUIFqKqUcogHWt5gyGvTgEhwBwBP1kYrVnBlhB2nuWHLYpJDI6b
uBoqKrHtrcdgXsKumSP0OKpn0nbYxknOvNYVjUUR6plMboUBaWX1oKoR6pNzTEHS
al4bIU7XMwppkR3KNQIDAQAB
-----END PUBLIC KEY-----`;
function getSpkiDer(spkiPem) {
const pemHeader = "-----BEGIN PUBLIC KEY-----";
const pemFooter = "-----END PUBLIC KEY-----";
var pemContents = spkiPem.substring(
pemHeader.length,
spkiPem.length - pemFooter.length
);
var binaryDerString = window.atob(pemContents);
return str2ab(binaryDerString);
}
async function importPublicKey(spkiPem) {
return await window.crypto.subtle.importKey(
"spki",
getSpkiDer(spkiPem),
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
["encrypt"]
);
}
async function encryptRSA(key, plaintext) {
let encrypted = await window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
},
key,
plaintext
);
return encrypted;
}
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
async function encrypt(plaintext) {
const pub = await importPublicKey(publicKey);
const encrypted = await encryptRSA(
pub,
new TextEncoder().encode(plaintext)
);
const encryptedBase64 = window.btoa(ab2str(encrypted));
console.log(encryptedBase64);
}
encrypt("I want to decrypt this string in C#");
})();
However: If I want to decrypt the code in my backend again, this fails
private static void Decrypt()
{
var name = "test";
var encryptedBase64 = @"Rzabx5380rkx2+KKB+HaJP2dOXDcOC7SkYOy4HN8+Nb9HmjqeZfGQlf+ZUa6uAfAJ3oAB2iIlHlnx+iXK3XDIX3izjoW1eeiNmdOWieNCu6YXqW4denUVEv0Z4EpAmEYgVImnEzoMdmPDEcl9UHgdWUmS4Bnq6T8Yqh3UZ/4NOc=";
var encrypted = Convert.FromBase64String(encryptedBase64);
using var privateKey = new RSACryptoServiceProvider();
privateKey.FromXmlString(File.ReadAllText(name + "_priv.xml"));
var decryptedBytes = privateKey.Decrypt(encrypted, false);
var dectryptedText = Encoding.UTF8.GetString(decryptedBytes);
}
I tried privateKey.Decrypt(encrypted, false); which throws
Internal.Cryptography.CryptoThrowHelper+WindowsCryptographicException: Wrong parameter
CapiHelper.DecryptKey(SafeKeyHandle safeKeyHandle, Byte[] encryptedData, Int32 encryptedDataLength, Boolean fOAEP, Byte[]& decryptedData)
RSACryptoServiceProvider.Decrypt(Byte[] rgb, Boolean fOAEP)
and privateKey.Decrypt(encrypted, false); which throws
System.Security.Cryptography.CryptographicException: Cryptography_OAEPDecoding,
CapiHelper.DecryptKey(SafeKeyHandle safeKeyHandle, Byte[] encryptedData, Int32 encryptedDataLength, Boolean fOAEP, Byte[]& decryptedData)
RSACryptoServiceProvider.Decrypt(Byte[] rgb, Boolean fOAEP)
Note: I verified that I can sucessfully encrypt/decrypt data in the backend with the private_key/public keys in xml format.
And I verified that I can sucessfully encrypt/decrypt data in the frontend with the
private/public keys in PEM format.
The thing that does not work is decrypt a string in C# that is encrypted in the frontend with javascript.
What am I doing wrong?
For reference (Just generated for this purpose)
-----BEGIN PRIVATE KEY-----
MIICeQIBADANBgkqhkiG9w0BAQEFAASCAmMwggJfAgEAAoGBAMbLxu2tudI0/Pu9
Yp9BcrEpfODbxkPyMIB3uAZh9QgWoqpRyiAda3mDIa9OASHAHAE/WRitWcGWEHae
5YctikkMjpu4Gioqse2tx2Bewq6ZI/Q4qmfSdtjGSc681hWNRRHqmUxuhQFpZfWg
qhHqk3NMQdJqXhshTtczCmmRHco1AgMBAAECgYEAokAVN02wOQm4ZPp4cMSpCEF1
Q8z8L96OiXusvcDbjWN0FhC1KKr6We2V44+FyvcRpE8At+xcMmz5OOeNLFwV3QLZ
GOYjZXP5dmRC3mG7HOv0Iu4QqAQCMEzLf998+6RwA24U74ysm+6CVCeVWZLtJSi/
UdQm3jho086iQF9UOo0CQQDjjhZl/fOqqb9nvW3rvSNwsdzSYoGpfx22uzrJplN2
wpFO6XCorAGMO6lHI3Ua8A0OSNO1ybkhG2iZOkPoEGWHAkEA36VhsUFNQr4RO7gL
oWpB+D2QtciZjnHm+QGRlfDl1mq527LHnHURrBQVRcHR3OgQbJ1wsSi4IjcKJ3l6
EtcBYwJBAKRTtIsc1D0XbljdLCcEJDa6yuvHJTmgyXVvSenbSgTGRycEX03/QPLj
FsB/s46rcdIx92kc7qsg3u1gbS+Fv7sCQQC5QHaxqxPiayo/O2524FuQ0v5hda6s
rXDTZhdACnF3sKQPdgGeeeKPlXshczDxOVERh0BnnwEXZlwE4rzZijtdAkEA2gXb
e/4gNIAuowBdgs1nXtuLKTP/HJzPIfil6zcF82Jc5dy7lR7nJCl088w0t1a0ebx5
LrC2qjX4SMEUbMTkNg==
-----END PRIVATE KEY-----`
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGy8btrbnSNPz7vWKfQXKxKXzg
28ZD8jCAd7gGYfUIFqKqUcogHWt5gyGvTgEhwBwBP1kYrVnBlhB2nuWHLYpJDI6b
uBoqKrHtrcdgXsKumSP0OKpn0nbYxknOvNYVjUUR6plMboUBaWX1oKoR6pNzTEHS
al4bIU7XMwppkR3KNQIDAQAB
-----END PUBLIC KEY-----
<RSAKeyValue><Modulus>xsvG7a250jT8+71in0FysSl84NvGQ/IwgHe4BmH1CBaiqlHKIB1reYMhr04BIcAcAT9ZGK1ZwZYQdp7lhy2KSQyOm7gaKiqx7a3HYF7Crpkj9DiqZ9J22MZJzrzWFY1FEeqZTG6FAWll9aCqEeqTc0xB0mpeGyFO1zMKaZEdyjU=</Modulus><Exponent>AQAB</Exponent><P>444WZf3zqqm/Z71t670jcLHc0mKBqX8dtrs6yaZTdsKRTulwqKwBjDupRyN1GvANDkjTtcm5IRtomTpD6BBlhw==</P><Q>36VhsUFNQr4RO7gLoWpB+D2QtciZjnHm+QGRlfDl1mq527LHnHURrBQVRcHR3OgQbJ1wsSi4IjcKJ3l6EtcBYw==</Q><DP>pFO0ixzUPRduWN0sJwQkNrrK68clOaDJdW9J6dtKBMZHJwRfTf9A8uMWwH+zjqtx0jH3aRzuqyDe7WBtL4W/uw==</DP><DQ>uUB2sasT4msqPztuduBbkNL+YXWurK1w02YXQApxd7CkD3YBnnnij5V7IXMw8TlREYdAZ58BF2ZcBOK82Yo7XQ==</DQ><InverseQ>2gXbe/4gNIAuowBdgs1nXtuLKTP/HJzPIfil6zcF82Jc5dy7lR7nJCl088w0t1a0ebx5LrC2qjX4SMEUbMTkNg==</InverseQ><D>okAVN02wOQm4ZPp4cMSpCEF1Q8z8L96OiXusvcDbjWN0FhC1KKr6We2V44+FyvcRpE8At+xcMmz5OOeNLFwV3QLZGOYjZXP5dmRC3mG7HOv0Iu4QqAQCMEzLf998+6RwA24U74ysm+6CVCeVWZLtJSi/UdQm3jho086iQF9UOo0=</D></RSAKeyValue>
<RSAKeyValue><Modulus>xsvG7a250jT8+71in0FysSl84NvGQ/IwgHe4BmH1CBaiqlHKIB1reYMhr04BIcAcAT9ZGK1ZwZYQdp7lhy2KSQyOm7gaKiqx7a3HYF7Crpkj9DiqZ9J22MZJzrzWFY1FEeqZTG6FAWll9aCqEeqTc0xB0mpeGyFO1zMKaZEdyjU=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>
Just to make it clear what I want to achive. I am aware that using a private in a browser might be a bad idea. But in my case this is required:
Internal webapp that needs to write encrypted data to an NDEF Tag using Web NFC API and is the one that needs the private key.
Webapp that reads the encrypted data from the NEDF tag and transfers it to a .NET Webapp (App 3)
Reads the encrypted data from App 2 and decrypts it.
Both codes use different paddings, on the JavaScript side OAEP (with SHA256), on the C# side PKCS#1 v1.5. For decryption to be possible on the C# side, OAEP with SHA256 must be used there as well.
You have not specified a .NET version. Under .NET Core 3.0+ or .NET 5+ decryption is possible e.g. with:
...
using var privateKey = RSA.Create();
...
var decryptedBytes = privateKey.Decrypt(encrypted, RSAEncryptionPadding.OaepSHA256);
...
and gives as plaintext for the posted ciphertext and keys: The bunny hops at teatime.
RSACryptoServiceProvider.Decrypt(Byte[], Boolean) uses PKCS#1 v1.5 if the second parameter is set to false. If the second parameter is set to true OAEP is applied, but with SHA1.
RSACryptoServiceProvider.Decrypt(Byte[], RSAEncryptionPadding) allows setting OAEP with SHA256, but a runtime error occurs because only SHA1 is supported (s. Remarks).
Therefore a change to e.g. RSA.Decrypt(Byte[], RSAEncryptionPadding) is necessary for OAEP with SHA256.
In addition to @Topaco's correct answer, decryption is still possible on older versions of .NET by changing the hashing algorithm to SHA-1 in both Javascript and C# thusly:
async function importPublicKey(spkiPem) {
return await window.crypto.subtle.importKey(
"spki",
getSpkiDer(spkiPem),
{
name: "RSA-OAEP",
hash: "SHA-1", //// <- SHA-1 here for older .NET decryption
},
true,
["encrypt"]
);
}
and
using (var privateKey = RSA.Create())
{
privateKey.FromXmlString(privKey);
var dectryptedBytes = privateKey.Decrypt(encrypted, RSAEncryptionPadding.OaepSHA1);
// ^^^^^^^^^ OaepSHA1 here for older versions of .NET ^^^^^^
var dectryptedText = Encoding.UTF8.GetString(dectryptedBytes);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With