Search code examples
node.jsflutterdartrsa

How to Replicate RSA Authentication from Node.js in Flutter?


I have the code below working in node.js and I am trying to convert it to make the API call directly from my flutter app, but I am having problems with the RSA encryption.

import fetch from "node-fetch";
import nodeRSA from "node-rsa";

const KEYVER = '23'
const ID = '123456789123456789'
const PRIVATE_KEY = "vvkmlkmmvcmemmcmdmdmm.......cddncndndncn ="

generateRequestHeader(){
const hashString = `${ID}\n{Date.now().toString()}\n{KEYVER}\n`;
const signer = new nodeRSA(PRIVATE_KEY, "pkcs1");
const signature = signer.sign(hasString);
const sign_enc = signature.toString("base64");

return {
    "AUTH_SIGNATURE": sign_enc,
    "TIMESTAMP": Date.now().toString(),
    "ID": ID,
    "KEY_VERSION":KEYVER
  };
}

async function callAPI(){
  const options = {
     method: 'GET',
     headers: generateRequestHeader()
 };

 const response = await fetch(url, options);
 return response;
}

The authentication works fine in Node.js but I can't seem to find a package to replicate it in flutter. I was recommended fast_rsapackage:

#fast_rsa: ^3.4.6
import 'package:fast_rsa/fast_rsa.dart';

 class Signature{
   String Id = 'c93e7094-327b-4ff3-bf2e-c52f29a8277f';
   String privateKey = "ABCDEG....Z=";
   String keyVer = '23.0';

   generateRequestHeaders() async {
      String timeStamp = DateTime.now().toString();
      String hashString = "${Id}\n${timeStamp}\n${keyVer}\n";


     var signer = await RSA.convertPrivateKeyToPKCS1(privateKey);
     var signature = await RSA.signPKCS1v15(signer, Hash.SHA256, privateKey);
     var signature_enc = await RSA.base64(signature);

     return {

         "AUTH_SIGNATURE": signature_enc,
         "TIMESTAMP": timeStamp,
         "ID": Id,
         "KEY_VERSION": keyVer,
    };
 }

 Future<dynamic> rsaRequest() async {
    var options = {'method': 'GET', 'headers': generateRequestHeaders()};

   String url = 'https://api.........';
   http.Response response = await http.get(url, headers: options);

   try {
     if (response.statusCode == 200) {
       print(response.body);
       var document = parse(response.body);
       return document;
    } else {
      return "failed";
    }
  } catch (exp) {
    print(exp);
    return "failed";
  }
 }
  
}

But the server keeps returning auth_error.

How do I use the .js function directly inside flutter?


Solution

  • I focus on the signing part. The NodeJS code creates a signature using RSA. For padding and digest the node-rsa default values are applied: PKCS#1v1.5 padding and SHA256, s. here. The private key is imported as DER encoded PKCS#1 key (Base64 encoded). The signature is Base64 encoded.

    Note that in the NodeJS code posted in the question, the $ signs for the 2nd and 3rd variables regarding hashString are missing, which is probably a copy/paste error. This must be fixed, otherwise the signatures will differ!

    On the Dart side, the following fixes are needed:

    • The PKCS#1 key is to be passed directly to RSA.signPKCS1v15(), i.e. the RSA.convertPrivateKeyToPKCS1() call is to be removed. RSA.signPKCS1v15() expects a PEM encoded key, i.e. header and footer are to be added and in the Base64 encoded body there is a line break after every 64 characters.
    • The timestamp is to be converted to the format used in the NodeJS code: DateTime.now().millisecondsSinceEpoch.toString().
    • RSA.signPKCS1v15() returns the signature already base64 encoded, i.e. the RSA.base64() call must be removed.

    A possible dart counterpart with the fast_rsa library that fixes the above issues is:

    Future<Map<String,String>> generateRequestHeaders() async {
        String privateKey = '''-----BEGIN RSA PRIVATE KEY-----
    MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+
    04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQT
    HIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssP
    FNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211q
    SIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybj
    BAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAf
    WWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ==
    -----END RSA PRIVATE KEY-----''';
        String  keyVer = "23";
        String  Id = "123456789123456789";
        String timeStamp = DateTime.now().millisecondsSinceEpoch.toString(); // "1649917884089" for testing
        String hashString = "${Id}\n${timeStamp}\n${keyVer}\n";
        String signature = await RSA.signPKCS1v15(hashString, Hash.SHA256, privateKey);
        return {
            "AUTH_SIGNATURE": signature,
            "TIMESTAMP": timeStamp,
            "ID": Id,
            "KEY_VERSION": keyVer,
        };
    }
    ...
    var result = await generateRequestHeaders();
    print(result["AUTH_SIGNATURE"]); // nRuX6eY+66Ca2ZbB/ZK6ealRdS8gYJ4UKNwUOdJySqujGnwpflE8aZ45L4PfQK3qAMJh02o0SVG8uy2Mz+BFpg== for datetime = '1649917884089'
    

    Test:
    Since signing with PKCS#1 v1.5 is deterministic, the same input data provides the same signature. This makes it easy to check the functional equivalence of both codes. If the same timestamp is used in both codes (e.g. the commented out 1649917884089), both codes return the same signature (nRuX6eY+66Ca2ZbB/ZK6ealRdS8gYJ4UKNwUOdJySqujGnwpflE8aZ45L4PfQK3qAMJh02o0SVG8uy2Mz+BFpg==), which proves the equivalence of both codes.

    This is the fixed NodeJS code used for the test. It is essentially the same as the NodeJS code posted in the question:

    // DER encoded PKCS#1 key, Base64 encoded
    // Note: For testing purposes, a 512 bits key is used. In practice, key sizes >= 2048 bits must be applied for security reasons!
    const PRIVATE_KEY = "MIIBOwIBAAJBANoHbFSEZoOSB9Kxt7t8PoBwmauaODjECHqJgtTU3h4MW5K3857+04Flc6x6a9xxyvCKS5RtOP2gaOlOVtrph0ECAwEAAQJBALu8LpRr2RWrdV7/tfQTHIJd8oQnbAe9DIvuwh/fF08IwApOE/iGL+Ded49eoHHu1OXycZhpHavN/sQMnssPFNECIQDyDIW7V5UUu16ZAeupeQ7zdV6ykVngd0bb3FEn99EchQIhAOaYe3ll211qSIXVjKHudMn3xe6Vvguc9O7cwCB+gyqNAiEAsr3kk6/de23SMZNlf8TR8Z8eyybjBAuQ3BMaKzWpyjECIFMR0UFNYTYIyLF12aCoH2h2mtY1GW5jj5TQ72GFUcktAiAfWWXnts7m8kZWuKjfD0MQiW+w4iAph+51j+wiL3EMAQ=="
    const KEYVER = '23';
    const ID = '123456789123456789';
    const timeStamp = Date.now().toString(); // '1649917884089' for testing
    
    function generateRequestHeader(){
        const hashString = `${ID}\n${timeStamp}\n${KEYVER}\n`; // Fix: Add the $ sign
        const signer = new nodeRSA(PRIVATE_KEY, "pkcs1");
        const signature = signer.sign(hashString); // default signing scheme: PKCS#1 v1.5 with SHA256
        const sign_enc = signature.toString("base64");    
        return {
            "AUTH_SIGNATURE": sign_enc,
            "TIMESTAMP": Date.now().toString(),
            "ID": ID,
            "KEY_VERSION":KEYVER
        };
    }
    ...
    var result = generateRequestHeader();
    console.log(result.AUTH_SIGNATURE); // nRuX6eY+66Ca2ZbB/ZK6ealRdS8gYJ4UKNwUOdJySqujGnwpflE8aZ45L4PfQK3qAMJh02o0SVG8uy2Mz+BFpg== for datetime = '1649917884089'