7

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.

Jürgen Steinblock
  • 30,746
  • 24
  • 119
  • 189
  • 2
    You might want to consider adapting your title to your question. "Encode" != "Encrypt". – Fildor Jan 24 '22 at 15:18
  • 1
    You're right. Thanks. – Jürgen Steinblock Jan 24 '22 at 15:41
  • seems that your encryptedBase64 string is a little bit faulty, can't decode from base64 into something readable so I think the bug is when you build that base64 string instead the Crypt/decrypt – J.Salas Jan 24 '22 at 15:43
  • Is isn't clear _why_ you're manually encrypting in JavaScript in-browser. Moreover, communication between browser and back-end might not be as secure as you believe, in account of anybody being able to jam arbitrary data in to the JS function. The only scenario by which this might be viable is that where the browser is in a known-secure environment, the server is in a known-secure environment and the intermediate connection is known to be insecure. Please clarify your intended usage scenario. – CJPN Jan 24 '22 at 16:25
  • Surely transmitting to an https:// does this for you automagically - a standard created by people far cleverer [sic] than a manual implementation. – CJPN Jan 24 '22 at 16:35
  • @CJPN I added some details why I need this. – Jürgen Steinblock Jan 24 '22 at 20:17
  • @JürgenSteinblock Thanks for the clarification concerning your usage scenario. Yes, that does indeed make sense. – CJPN Jan 26 '22 at 12:05
  • A final caveat emptor: Keep in mind that RSA alone can only encrypt a length/size of data up to its own key size :- 128 bytes for you demonstrated 1024-bit key, and so on. Larger quantities of data would likely require an RSA/AES combo where the RSA algorithm encrypts an AES key which, when encrypted, forms the first XYZ-bits of transmitted data whereas the remainder contains the payload. – CJPN Jan 26 '22 at 16:58
  • Thanks. Didn't know that. My data is 100% sure below the key length (in face 128 bytes is the data limit for my nfc213 tags) – Jürgen Steinblock Jan 27 '22 at 10:11

3 Answers3

7

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.

Topaco
  • 40,594
  • 4
  • 35
  • 62
2

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);
 }
CJPN
  • 1,497
  • 1
  • 9
  • 8
-2

You need to encrypt with the private key and then decrypt with the public key

Natalia Muray
  • 252
  • 1
  • 6
  • 1
    Exposing the private key in a public context would allow anybody to freely encrypt data using said key, thus, in all likelihood, completely devastating OP's initial intention. – CJPN Jan 24 '22 at 16:14
  • This applies to signing/verifying: When signing, the private key is used; when verifying, the public key is used. – Topaco Jan 24 '22 at 18:28
  • Yes, however the question isn't asking for a Message Authentication Code. – CJPN Jan 24 '22 at 18:33