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?
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:
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.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'