Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Encrypt data in Javascript, Decrypt data in C# using private/public keys

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)

private key

-----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-----`

public key

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGy8btrbnSNPz7vWKfQXKxKXzg
28ZD8jCAd7gGYfUIFqKqUcogHWt5gyGvTgEhwBwBP1kYrVnBlhB2nuWHLYpJDI6b
uBoqKrHtrcdgXsKumSP0OKpn0nbYxknOvNYVjUUR6plMboUBaWX1oKoR6pNzTEHS
al4bIU7XMwppkR3KNQIDAQAB
-----END PUBLIC KEY-----

private key xml

<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>

public key xml

<RSAKeyValue><Modulus>xsvG7a250jT8+71in0FysSl84NvGQ/IwgHe4BmH1CBaiqlHKIB1reYMhr04BIcAcAT9ZGK1ZwZYQdp7lhy2KSQyOm7gaKiqx7a3HYF7Crpkj9DiqZ9J22MZJzrzWFY1FEeqZTG6FAWll9aCqEeqTc0xB0mpeGyFO1zMKaZEdyjU=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>

Update

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:

App 1

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.

App 2

Webapp that reads the encrypted data from the NEDF tag and transfers it to a .NET Webapp (App 3)

App 3

Reads the encrypted data from App 2 and decrypts it.

like image 748
Jürgen Steinblock Avatar asked May 24 '26 14:05

Jürgen Steinblock


2 Answers

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.

like image 139
Topaco Avatar answered May 26 '26 03:05

Topaco


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);
 }
like image 37
CJPN Avatar answered May 26 '26 04:05

CJPN