node.jsrsaencryption-asymmetric

How to encrypt a string in browser and decrypt it in Node JS using RSA?


I know this should better be done with ECDH+AES, but I want to make things simplier as it is a hobby project.

I want to generate a key pair with Node and then give the public key to the client so that he can encrypt his login credentials. I want to be able to decrypt it using my private key in Node JS script.

I have the following client code:

const publicKeyHash = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx+6mliYZnAmJid8/u+SdiMdjB5BlNub4AsObrSG0v1AkmiiLL7N0WzN0+alClyXIOm12qQtYdHDaNkTr1xiO7ZYKH92y9S4g4PI5IIx6U7BNardEnvAS8YMh3HfuC/wMkkLiwuy/QvMPXL+Dp3eNzESJ69rMKkaEf5N2qZ39DbzA3//5PU7UlmhdjmMAr7h30cr5433mMR/NnuMJ+ZYWbZnlaeJKZFjWO2EkuzN2cheaIVxgi46pfdLnZfdxQE8TARgenw/5KdkVzaIk0y+eShEs+YA7hLYTI3iohthY0hgMv1+dd3TYzp/Nc3yqsyngXeQxa2pi4xTlmkXEiLtBeQIDAQAB';
 
const 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;
}
 
const generateKey =  async () => await window.crypto.subtle.generateKey(
    {
        name: "RSA-OAEP",
        modulusLength: 2048,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: { name: "SHA-256" }
    },
    true,
    ["encrypt", "decrypt"]
);
 
const importKey = async (pem, opt) => {
    const binaryDerString = window.atob(pem);
    const binaryDer = str2ab(binaryDerString);
    return await window.crypto.subtle.importKey(
        "spki",
        binaryDer,
        {
            name: "RSA-OAEP",
            hash: "SHA-256"
        },
        true,
        opt
    );
}
 
const encryptMessage = async (key, msg) => {
    return await window.crypto.subtle.encrypt(
        {
            name: "RSA-OAEP"
        },
        key,
        str2ab(msg)
    )
}
 
const decryptMessage = async (key, msg) => {
    return await window.crypto.subtle.decrypt(
        {
          name: "RSA-OAEP"
        },
        key,
        msg
    );
}
 
function arrayBufferToBase64String(arrayBuffer) {
    var byteArray = new Uint8Array(arrayBuffer)
    var byteString = ''
    for (var i=0; i<byteArray.byteLength; i++) {
      byteString += String.fromCharCode(byteArray[i])
    }
    return btoa(byteString)
}
 
const test = async _ => {
    try{
        let publicKey = await importKey(publicKeyHash, ["encrypt"]);
 
        let msg = "Admin:Password";
        console.log("исходный текст для шифрования:", msg);
        let msgEncrypted = await encryptMessage(publicKey,  msg);
        console.log(arrayBufferToBase64String(msgEncrypted));

    }
    catch(e){
        console.log(e)
    }
}

test()

and the NodeJS code:

const crypto = require("crypto");

/*
crypto.generateKeyPair('rsa', {
    modulusLength: 2048,
    publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
    },
    privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem'
    }
}, (err, publicKey, privateKey) => {
    console.log(publicKey);
    console.log(privateKey);
});
*/

let privateKey = crypto.createPrivateKey('-----BEGIN PRIVATE KEY-----\
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDH7qaWJhmcCYmJ3z+75J2Ix2MHkGU25vgCw5utIbS/UCSaKIsvs3RbM3T5qUKXJcg6bXapC1h0cNo2ROvXGI7tlgof3bL1LiDg8jkgjHpTsE1qt0Se8BLxgyHcd+4L/AySQuLC7L9C8w9cv4Ond43MRInr2swqRoR/k3apnf0NvMDf//k9TtSWaF2OYwCvuHfRyvnjfeYxH82e4wn5lhZtmeVp4kpkWNY7YSS7M3ZyF5ohXGCLjql90udl93FATxMBGB6fD/kp2RXNoiTTL55KESz5gDuEthMjeKiG2FjSGAy/X513dNjOn81zfKqzKeBd5DFramLjFOWaRcSIu0F5AgMBAAECggEBAIARQuxTI3xsL4n9U1oMK0z+S1UBDZSJhrxeiE3ylVEnCQsmhWHc1d/FqlxyK5GeRhAHJkdKWTLdRyoUI+34cTWHMp0O6h9bmPv+rNFIquMIv85n7rDQn5HMqdgLipRqs7LM3Wx3Mly0TVbf5nlTf6UlEiPeV4GNAjqmPxCPfxVYe4jVncxYtGEpbid5gs5VU1kat411vmYNUZW7RjH1GYdI1nkuxjiNdMK3QvWGFj1ABkHGXpAyCG97w9zIdeCytBLPZ7HIF+0wIE+6DAu9KdG3uKbAf5td2FQiVsk9gBp+oG+0vfs/jo9f7Xq4TopMPTWOQuRFWGNu8O+dZfKS4DECgYEA/Z7pHL9CVkL9NcsXwHgnSEYMr5bwwFdeQ0T2zzEg9nAFZNOnAkLMmhC1jWr9ozT9esC4cpDmYRamd9HuEATIQi/ETvrbeERUYZAq55cEVxvZJwvGtOlKaW3bH7k+O+kdl07jvuGWsnqH3QSg/ISttwyc99RT9XjbrYKEex2E518CgYEAyc7NmCgltgCxPUWHJSRrDMWKhnM0pwju1vMgk5a/8Kqnwv7X+7aWnkXFMWu3/D8jIF3bFEKDpC7ZlAq0WwFfBtaGs/9WRtvbzP/cM5CPWeu9XOW9x7Yx2rn3IocU0uDvwcYedcVHDRa05nC5t9+6lcGC++kLkZWY+2mzJ2ptPicCgYEA1NYE3uEKdIWnJPuYpSawIJDYmIpc46zuKBm53cpm1SjQ/fo4j0crmKcpFNKSo+IWTmto3owHKbbuYGNGGx9IR3L6lSUkBNuizHVF7C/prohqRcA2MyAMGEnet9KnDXPmJ1JHAasi4gi995ao2wElHxZwq/H9u2R/Ri7fqsns/JUCgYA4RWPqg3dQcoz5SsPORYNcAlEIAGO0F7eRVCXHQYevscYphynuVBFXfJjpmOGqgmhnBASsd4eBZlbhAsMbhRxcKcFRu2bxRyjY1pcuAacXKbaZiq4KW/E2zhftFsFls7bmzzl7GVEggy5Z+yCt9sJuM5E0cNz68T3BDM4ZtYFUjQKBgQCvFU70gdcWhM4uYmSe3L4VBxdS53SaVIdtGCVKNCK5ZaJPqj+g5vL9KMpq9cQ4rl/rgGqo45WKNACvS3Q42+bMLyjUmMPyU9lLScp49RasXmK3sw/2CmQAsky793Zcf9MYIp1f11rcJBTlKrz3ZvBkjYeYpVAtWraB6sejZmPJGg==\
-----END PRIVATE KEY-----');

let data = 'PjEhLCjoPLc3ZXFE1VNga00KHFif+wtNfjIGCck/4SeLn3woDBhHP4cJCRdyqPajLLtUVMWUDZpEXfAh4Ch5hAB3+qI6mo+6n9NDBHv6/jH+sF7rl5XQCcbZyVOSIWtmzPr//eIWNUheg0YXweX9mgC9KeUpXvtIAY7LT5tnvaZpYJx1zyX3YP/4eIisjVMJbhuM1K8sakvAXparOwWg/TpCDPiFN68OOuWTgBju4tjbSy8iLKy5RecxhvuI1M6/4i2Cx3FWxqcNPgIgcjxXTzAPjYz3cqqDqQ4jORsHoJ9yOovWtNpP8xF4QwNj5n4Ci5mRmfs2g2Zs4stGvx1CEg==';



const decryptedData = crypto.privateDecrypt(
   privateKey,
   Buffer.from(data, 'base64')
);
console.log(decryptedData.toString());

For some reason I have the following error:

Error: error:04099079:rsa routines:RSA_padding_check_PKCS1_OAEP_mgf1:oaep decoding error at Object.privateDecrypt (node:internal/crypto/cipher:79:12) at Object.<anonymous> (/home/runner/Generate-Key-Pair/index.js:27:30) at Module._compile (node:internal/modules/cjs/loader:1101:14)

If I try to decrypt with no padding (I guess it is even theoretically wrong), it gives me no errors but the result is not correct:

const decryptedData = crypto.privateDecrypt({
      key: privateKey,
      passphrase: '',
      padding: crypto.constants.RSA_NO_PADDING
   },
   Buffer.from(data, 'base64')
);
console.log(Buffer.from(decryptedData));

I'm sure that 'RSA-OAEP' should work in NodeJS. As my keys are generated with Node too, I guess the problem is with the encryption in browser. But this 'RSA-OAEP' mode is the only one supported for encrypt/decrypt (the second one is for sign/verify).

What am I doing wrong?


Solution

  • In the NodeJS code, SHA-1 is used for the OAEP digests by default. Since you apply SHA-256 in the WebCrypto code, you must explicitly specify SHA-256 in the NodeJS code using oaepHash.

    Modify your NodeJS code as follows:

    ...
    const decryptedData = crypto.privateDecrypt(
       {
           key: privateKey,
           oaepHash: 'SHA256'
       },
       Buffer.from(data, 'base64')
    );
    ...
    

    With this change decryption with the NodeJS code works.