Search code examples
javascriptnode.jsencryptionopen-source

Getting the same result from two packages (jsencrypt and node-rsa)


We were using jsencrypt on the client-side of an application to encrypt a string (a password) and pass it over to the backend that handles 3rd party API authorisation.

Like so:

const n = "_key"

export const encrypt2 = async(message: string): Promise < string > => {
  const JSEncrypt = (await
    import ('jsencrypt')).default;
  const jsencrypt = new JSEncrypt({
    default_key_size: '1024',
    default_public_exponent: "010001",
    log: false,
  });
  jsencrypt.setPublicKey(n);
  return jsencrypt.encrypt(message) || message;
};

Due to product changes, we now want the encryption on the backend (nodejs). After research we found node-rsa that can have the same result. Used the code below:

export const encrypt = async(message: string): Promise < string > => {
  return new NodeRSA().generateKeyPair(1024, '010001').importKey(n, 'public').encrypt(message).toString('base64')
};

given that n is still the same, our 3rd party auth service rejects this implementation.

Question & Goal:

How can I get a valid encrypted password from both of these implementations?

P.S, I was unable to use jsencrypt on the server side due to window usage


Solution

  • JSEncrypt only supports PKCS#1 v1.5 padding (see here), while Node-RSA supports PKCS#1 v1.5 padding and OAEP, where OAEP is the default (see here). The different paddings are the reason why both codes are not compatible.

    In order for PKCS#1 v1.5 padding to be used in the Node-RSA code, this must be explicitly specified, e.g. in the 3rd parameter when importing the key:

    const NodeRSA = require('node-rsa');
    
    var x509 = `-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunF5aDa6HCfLMMI/MZLT
    5hDk304CU+ypFMFiBjowQdUMQKYHZ+fklB7GpLxCatxYJ/hZ7rjfHH3Klq20/Y1E
    bYDRopyTSfkrTzPzwsX4Ur/l25CtdQldhHCTMgwf/Ev/buBNobfzdZE+Dhdv5lQw
    KtjI43lDKvAi5kEet2TFwfJcJrBiRJeEcLfVgWTXGRQn7gngWKykUu5rS83eAU1x
    H9FLojQfyia89/EykiOO7/3UWwd+MATZ9HLjSx2/Lf3g2jr81eifEmYDlri/OZp4
    OhZu+0Bo1LXloCTe+vmIQ2YCX7EatUOuyQMt2Vwx4uV+d/A3DP6PtMGBKpF8St4i
    GwIDAQAB
    -----END PUBLIC KEY-----`;
    
    var message = "The quick brown fox jumps over the lazy dog";
    var privateKey = new NodeRSA(x509, 'public', { encryptionScheme: 'pkcs1' }); // specify PKCS#1 v1.5 padding
    
    var ciphertext = privateKey.encrypt(message).toString('base64');
    console.log("Ciphertext (Node-RSA): ", ciphertext);
    

    A possible output is:

    Ciphertext (Node-RSA):  Lcfy7G0RYhDRCeWAvUXji4b4uFzrhpSsS9G158QcBiRy+Uae5UctQoA6MMgzZsjVWmf4Jlj50eZYTwxP94mGrTY0gMy9+FvFlMXDu0cMJ04qArOU4B+ZHnt3FElDjYvW0fnvbstYJFWd9OQjlLJc2mxSGPKEj0IQi3p8GQXK9qYfJx7MeONA+nfHdWYQgplicnz1crk+aiGf0zlJMAKdOoTodAtQuqVh9bNYyJcpPnzIt4eqUVW+wLEoXNqu14ibz1u5yMpCNX/+jBKug2fYaXv4ImsUnhqFEAok9sHDGVzZkihnYdDxnWaSocpG3db6UNhpGBoJSvwxdEuI86YgSQ==
    

    Test:

    Since both OAEP and PKCS#1v1.5 padding are not deterministic, testing by comparing the generated ciphertexts is not possible. Instead, it can be checked whether the Node-RSA ciphertext can be decrypted with that JSEncrypt code which can also decrypt the JSEncrypt ciphertext. If this is the case, the encryption codes are functionally identical:

    var x509 = `-----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunF5aDa6HCfLMMI/MZLT
    5hDk304CU+ypFMFiBjowQdUMQKYHZ+fklB7GpLxCatxYJ/hZ7rjfHH3Klq20/Y1E
    bYDRopyTSfkrTzPzwsX4Ur/l25CtdQldhHCTMgwf/Ev/buBNobfzdZE+Dhdv5lQw
    KtjI43lDKvAi5kEet2TFwfJcJrBiRJeEcLfVgWTXGRQn7gngWKykUu5rS83eAU1x
    H9FLojQfyia89/EykiOO7/3UWwd+MATZ9HLjSx2/Lf3g2jr81eifEmYDlri/OZp4
    OhZu+0Bo1LXloCTe+vmIQ2YCX7EatUOuyQMt2Vwx4uV+d/A3DP6PtMGBKpF8St4i
    GwIDAQAB
    -----END PUBLIC KEY-----`;
    
    var pkcs8 = `-----BEGIN PRIVATE KEY-----
    MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC6cXloNrocJ8sw
    wj8xktPmEOTfTgJT7KkUwWIGOjBB1QxApgdn5+SUHsakvEJq3Fgn+FnuuN8cfcqW
    rbT9jURtgNGinJNJ+StPM/PCxfhSv+XbkK11CV2EcJMyDB/8S/9u4E2ht/N1kT4O
    F2/mVDAq2MjjeUMq8CLmQR63ZMXB8lwmsGJEl4Rwt9WBZNcZFCfuCeBYrKRS7mtL
    zd4BTXEf0UuiNB/KJrz38TKSI47v/dRbB34wBNn0cuNLHb8t/eDaOvzV6J8SZgOW
    uL85mng6Fm77QGjUteWgJN76+YhDZgJfsRq1Q67JAy3ZXDHi5X538DcM/o+0wYEq
    kXxK3iIbAgMBAAECggEASlJj0ExIomKmmBhG8q8SM1s2sWG6gdQMjs6MEeluRT/1
    c2v79cq2Dum5y/+UBl8x8TUKPKSLpCLs+GXkiVKgHXrFlqoN+OYQArG2EUWzuODw
    czdYPhhupBXwR3oX4g41k/BsYfQfZBVzBFEJdWrIDLyAUFWNlfdGIj2BTiAoySfy
    qmamvmW8bsvc8coiGlZ28UC85/Xqx9wOzjeGoRkCH7PcTMlc9F7SxSthwX/k1VBX
    mNOHa+HzGOgO/W3k1LDqJbq2wKjZTW3iVEg2VodjxgBLMm0MueSGoI6IuaZSPMyF
    EM3gGvC2+cDBI2SL/amhiTUa/VDlTVw/IKbSuar9uQKBgQDd76M0Po5Lqh8ZhQ3o
    bhFqkfO5EBXy7HUL15cw51kVtwF6Gf/J2HNHjwsg9Nb0eJETTS6bbuVd9bn884Jo
    RS986nVTFNZ4dnjEgKjjQ8GjfzdkpbUxsRLWiIxuOQSpIUZGdMi2ctTTtspvMsDs
    jRRYdYIQCe/SDsdHGT3vcUCybwKBgQDXDz6iVnY84Fh5iDDVrQOR4lYoxCL/ikCD
    JjC6y1mjR0eVFdBPQ4j1dDSPU9lahBLby0VyagQCDp/kxQOl0z2zBLRI4I8jUtz9
    /9KW6ze7U7dQJ7OTfumd5I97OyQOG9XZwKUkRgfyb/PAMBSUSLgosi38f+OC3IN3
    qlvHFzvxFQKBgQCITpUDEmSczih5qQGIvolN1cRF5j5Ey7t7gXbnXz+Umah7kJpM
    IvdyfMVOAXJABgi8PQwiBLM0ySXo2LpARjXLV8ilNUggBktYDNktc8DrJMgltaya
    j3HNd2IglD5rjfc2cKWRgOd7/GlKcHaTEnbreYhfR2sWrWLxJOyoMfuVWwKBgFal
    CbMV6qU0LfEo8aPlBN8ttVDPVNpntP4h0NgxPXgPK8Pg+gA1UWSy4MouGg/hzkdH
    aj9ifyLlCX598a5JoT4S0x/ZeVHd/LNI8mtjcRzD6cMde7gdFbpLb5NSjIAyrsIA
    X4hxvpnqiOYRePkVIz0iLGziiaMbfMwlkrxvm/LRAoGBALPRbtSbE2pPgvOHKHTG
    Pr7gKbmsWVbOcQA8rG801T38W/UPe1XtynMEjzzQ29OaVeQwvUN9+DxFXJ6Yvwj6
    ih4Wdq109i7Oo1fDnMczOQN9DKch2eNAHrNSOMyLDCBm++wbyHAsS2T0VO8+gzLA
    BviZm5AFCQWfke4LZo5mOS10
    -----END PRIVATE KEY-----`;
    
    var message = "The quick brown fox jumps over the lazy dog";
    
    var publicKey = new JSEncrypt();
    publicKey.setPublicKey(x509);
    var ciphertext = publicKey.encrypt(message);
    console.log("Ciphertext (JSEncrypt): ", ciphertext);
     
    var privateKey = new JSEncrypt();   
    privateKey.setPrivateKey(pkcs8);
    var decrypted = privateKey.decrypt(ciphertext);
    console.log("Decrypted (JSEncrypt): ", decrypted);
    
    var ciphertextFromNodeRSA = `Lcfy7G0RYhDRCeWAvUXji4b4uFzrhpSsS9G158QcBiRy+Uae5UctQoA6MMgzZsjVWmf4Jlj50eZYTwxP94mGrTY0gMy9+FvFlMXDu0cMJ04qArOU4B+ZHnt3FElDjYvW0fnvbstYJFWd9OQjlLJc2mxSGPKEj0IQi3p8GQXK9qYfJx7MeONA+nfHdWYQgplicnz1crk+aiGf0zlJMAKdOoTodAtQuqVh9bNYyJcpPnzIt4eqUVW+wLEoXNqu14ibz1u5yMpCNX/+jBKug2fYaXv4ImsUnhqFEAok9sHDGVzZkihnYdDxnWaSocpG3db6UNhpGBoJSvwxdEuI86YgSQ==`;
    
    decrypted = privateKey.decrypt(ciphertextFromNodeRSA);
    console.log("Decrypted (Node-RSA): ", decrypted);
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.2.1/jsencrypt.min.js"></script>

    A possible output:

    Ciphertext (JSEncrypt):  WWcnfnlAB7Quf/jMQIyD9AgOpewJQdPgA/UQFqIu9PvoMhMUgjo+4LnxeKjnFGqhG9mgctqmVKW1EGjEsnJiThbgiC6VNIwufYloXgJcqTrT78p7UiTO9+k7z1H8iCy4HkgXtiOkoBpGI094zr/iMnTyiTMfMKjopygM969pgDPsaXqWeiE2MX4ZjxFcNP9QpYoZpdS7PGxDBj31kbXvttOB/1Oc57IqOepF2N8n0jp9KzIBnAf+CDjzUFbUh8t23feuktoL/o/MYqqySmY/hkvkkmtNd4LDLtTA0NjcU7Es99ue90xNcuYKCEybzaZ20pp769PQjZHu/lU+T7zlrg== 
    Decrypted (JSEncrypt):  The quick brown fox jumps over the lazy dog 
    Decrypted (Node-RSA):  The quick brown fox jumps over the lazy dog
    

    proving that both encryption codes are functionally equivalent.