I’m attempting to authenticate to Coinbase Pro from a Google Script. I’ve managed to do this already in Postman using CryptoJS, but I’m running into issues generating the CB-ACCESS-SIGN signature header. I’ve set up a test using a test key and secret string to figure out the differences between CryptoJS.HmacSHA256 and Utilities.computeHmacSha256Signature, the implementation Google offers, and noticed a difference in parameters: CryptoJS.HmacSHA256 expects a secret as WordArray while Utilities.computeHmacSha256Signature expects a secret as string.
In Postman I'm doing the following to get the wordarray of my apiSecret to pass to CryptoJS.HmacSHA256:
var hash = CryptoJS.enc.Base64.parse(pm.variables.get('apiSecret'));
In my Google script I'm doing the sam
var hash = Utilities.base64Decode(apiSecretB64)
I've tried debugging this with the same secret and message, but I'm getting different results.
My implementation in Postman:
function computeSignature(request) {
const data = request.data;
const method = request.method.toUpperCase();
const path = getPath(request.url);
const body = (method === 'GET' || !data) ? '' : JSON.stringify(data);
const message = timestamp + method + path + body;
const apiSecret = CryptoJS.enc.Base64.parse(pm.variables.get('apiSecret'));
const hash = CryptoJS.HmacSHA256(message, apiSecret);
const hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
return hashInBase64;
}
And my implementation in Google Script:
function computeSignature(request, path, timestamp) {
const data = request.data;
const method = request.method;
const body = (method === 'GET' || !data) ? '' : JSON.stringify(data);
const message = timestamp + method + path + body;
var apiSecret = Utilities.base64Decode(apiSecretB64);
var hash = Utilities.computeHmacSha256Signature(message, apiSecret);
hash = Utilities.base64Encode(hash);
return hash;
}
Does anyone know why I'm getting different results?
I've managed to solve the issue by converting the message from string to a byte array:
function computeSignature(request, path, timestamp) {
const data = request.data;
const method = request.method;
const body = (method === 'GET' || !data) ? '' : JSON.stringify(data);
const message = timestamp + method + path + body;
var apiSecretByteArr = Utilities.base64Decode(apiSecretB64);
var messageByteArr = Utilities.base64Decode(Utilities.base64Encode(message));
var hash = Utilities.computeHmacSha256Signature(messageByteArr, apiSecretByteArr);
return Utilities.base64Encode(hash);
}
There is probably a better way of doing this, but at least the correct signature is now being computed.
Related
I am trying to call a Magneto 2 REST API with Oauth authentication. The call works in Postman, but when I call the API from Node-Red it says: "The signature is invalid. Verify and try again."
Here is a sample of the script that I created to generate the authorization header:
//Variables required from the function calling this flow
var p_http_method = "GET";
var p_url = "https://watchguard24.co.za/rest/V1/modules/";
//Static variables for Magento authorization
const p_oauth_version = "1.0";
const p_signature_method = "HMAC-SHA256";
const p_nonce = "17TTbZZM0fi" //encodeURIComponent(uuid.v4()); //"35WpXRWuoQp" //uuid.v4();
const p_consumer_key = "wypklfmtf6m53b0rfclxikr2xibopftu";
const p_access_token = "umnmupjtpidzuzz47vuo5s2ianxmq7cf";
const p_consumer_secret = encodeURIComponent("fw17d1k3i70zldcy7xvuvtjjykrzw286");
const p_token_secret = encodeURIComponent("lroq3mg3vteogfisez7hu2zwbu7fmq2x");
const p_timestamp = 1668081726 //Math.floor(Date.now() / 1000);
//Combining OAuth parameters
const parameters = {
oauth_consumer_key: p_consumer_key,
oauth_nonce: p_nonce,
oauth_signature_method: p_signature_method,
oauth_timestamp: p_timestamp,
oauth_version: p_oauth_version,
oauth_token: p_access_token
}
//Ordering and encoding the variables
let ordered = {};
Object.keys(parameters).sort().forEach(function (key) {
ordered[key] = parameters[key];
});
let encodedParameters = '';
for (var k in ordered) {
const encodedValue = encodeURIComponent(ordered[k]);
const encodedKey = encodeURIComponent(k);
if (encodedParameters === '') {
encodedParameters += encodeURIComponent(`${encodedKey}=${encodedValue}`)
}
else {
encodedParameters += encodeURIComponent(`&${encodedKey}=${encodedValue}`);
}
}
//Base signature string
const encodedUrl = encodeURIComponent(p_url);
encodedParameters = encodeURIComponent(encodedParameters);
const signature_base_string = encodeURIComponent(`${p_http_method}&${encodedUrl}&${encodedParameters}`)
//Create the oauth signature
const signing_key = `${p_consumer_secret}&${p_token_secret}`
const oauth_signature = crypto.createHmac("sha256", signing_key).update(signature_base_string).digest('base64');
//Encode oauth signature
const encoded_oauth_signature = encodeURIComponent(oauth_signature);
msg.headers = {
"Authorization":
'OAuth oauth_consumer_key=' + p_consumer_key
+ ',oauth_token=' + p_access_token
+ ',oauth_signature_method=' + p_signature_method
+ ',oauth_timestamp=' + p_timestamp
+ ',oauth_nonce=' + p_nonce
+ ',oauth_signature=' + encoded_oauth_signature
+ ',oauth_version=' + p_oauth_version
}
return msg;
I then changed the parameters in the script to match those of Postman but I am not getting the same signature that Postman is generating.
Here is the Postman authorization header details:
OAuth
oauth_consumer_key="wypklfmtf6m53b0rfclxikr2xibopftu",
oauth_token="d07jwep9s79srvy3dmjltlfafnpl9d1d",
oauth_signature_method="HMAC-SHA256",
oauth_timestamp="1668081726",
oauth_nonce="17TTbZZM0fi",
oauth_version="1.0",
oauth_signature="yktg8XIKp%2Fsjk2Wzr4Qk7Mxs6B7kY%2Fjx3040UvwHuY8%3D"
Here is the Node-Red (Node.js) authorization header generated:
OAuth
oauth_consumer_key=wypklfmtf6m53b0rfclxikr2xibopftu,
oauth_token=d07jwep9s79srvy3dmjltlfafnpl9d1d,
oauth_signature_method=HMAC-SHA256,
oauth_timestamp=1668081726,
oauth_nonce=17TTbZZM0fi,
oauth_version=1.0,
oauth_signature=8GPa253AV9I%2FIGB9N2s2Urbg7Km%2FUcjhzM%2BeRjuEag4%3D
Can anyone please assist with any potential fixes for my script that the correct oauth signature is generated? I have followed the Magento 2 documentation (https://developer.adobe.com/commerce/webapi/get-started/authentication/gs-authentication-oauth/) regarding the signature generation. I then looked at the following guidance but I still get the same error: https://pandeysoni.medium.com/how-to-create-oauth-1-0a-signature-in-node-js-7d477dead170
I need to be able to generate the same oauth signature as Postman to successfully call the REST API's and not get an invalid signature error.
I have been trying to communicate with the private API on kraken. The error I get suggests {"error":["EAPI:Invalid key"]} that the encryption/decryption steps are correct. I have tried creating new keys, does not help. I'm wondering if the 'format' of the signature variable is wrong, even though correct in nature.
function balance () {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("API_read_only");
var key = sheet.getRange("B5").getValue()
var secret = sheet.getRange("B6").getValue()
// (API method, nonce, and POST data)
var path = "/0/private/TradeBalance"
var nonce = new Date () * 1000
var postdata = "nonce=" + nonce
//Algorithms
//Calculate the SHA256 of the nonce and the POST data
// using goolge script lib
// using more succint function from https://stackoverflow.com/questions/16216868/get-back-a-string-representation-from-computedigestalgorithm-value-byte
function SHA_256 (str) {
return Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, str).reduce(function(str,chr){
chr = (chr < 0 ? chr + 256 : chr).toString(16);
return str + (chr.length==1?'0':'') + chr;
},'');
}
var api_sha256 = SHA_256(nonce + postdata)
//Decode the API secret (the private part of the API key) from base64 // need to stringyfy
var base64 = Utilities.base64Decode(secret)
var base64s = Utilities.newBlob(base64).getDataAsString()
//Calculate the HMAC of the URI path and the SHA256, using SHA512 as the HMAC hash and the decoded API secret as the HMAC key
var hamc512_uri = Utilities.computeHmacSha256Signature(path + api_sha256,base64s)
var hamc512_uris = Utilities.newBlob(hamc512_uri).getDataAsString()
//Encode the HMAC into base64
var signature = Utilities.base64Encode(hamc512_uris)
Logger.log(signature)
//An example of the algorithm using the variables shown above is as follows:
//Base64Encode(HMAC-SHA512 of ("/0/private/TradeBalance" + SHA256("1540973848000nonce=1540973848000&asset=xxbt")) using Base64Decode("FRs+gtq09rR7OFtKj9BGhyOGS3u5vtY/EdiIBO9kD8NFtRX7w7LeJDSrX6cq1D8zmQmGkWFjksuhBvKOAWJohQ==") as the HMAC key
//The result is the API-Sign value / signature.
// connect
var url = "https://api.kraken.com" + path;
var options = {
method: 'post',
headers: {
'API-Key': key,
'API-Sign': signature
},
payload: postdata
};
var response = UrlFetchApp.fetch (url, options);
json = response.getContentText ();
Logger.log(json)
}
While I cannot spot what's wrong with your code I faced the same problem as well (thinking I have everything correct but getting a EAPI:Invalid key) with different libraries.
The approach that helped me was:
Take some posted working solution, e.g. https://stackoverflow.com/a/43081507/672008 (in Java)
Check that it really works
Fix the nonce parameter to get a stable HMAC end results
Massage my code until I get then same intermediate & end results
In the end I was successful using this library: https://www.npmjs.com/package/jssha
The code:
import jssha from 'jssha';
const secret = '...';
const nonce = 1642383717038;
const message = '';
const path = '/0/private/Balance';
const data = 'nonce=' + nonce;
const dataHash = new jssha('SHA-256', 'TEXT');
dataHash.update(nonce + data + message);
let utf8Encode = new TextEncoder();
const hmacHash = new jssha('SHA-512', 'UINT8ARRAY', { hmacKey: { value: secret, format: 'B64' } });
hmacHash.update(utf8Encode.encode(path));
hmacHash.update(dataHash.getHash('UINT8ARRAY'));
console.log('hmac', hmacHash.getHash('B64'));
I'am trying to integrate the Zoom SDK into my application and I am having trouble figuring out how to convert their example code for generating the signature into ruby.
Example Code -
const base64JS = require('js-base64');
const hmacSha256 = require('crypto-js/hmac-sha256');
const encBase64 = require('crypto-js/enc-base64');
function generateSignature(data) {
let signature = '';
const ts = new Date().getTime();
const msg = base64JS.Base64.encode(data.apiKey + data.meetingNumber + ts + data.role);
const hash = hmacSha256(msg, data.apiSecret);
signature = base64JS.Base64.encodeURI(`${data.apiKey}.${data.meetingNumber}.${ts}.${data.role}.${encBase64.stringify(hash)}`);
return signature;
}
const data = {apiKey: "" ,
apiSecret: "",
meetingNumber: 888,
role: 0}
console.log(generateSignature(data));
How would the generateSignature function look like in ruby?
I've tried a few times but the outputted signature differed when I tried writing this in Ruby. I suspect that I'am encoding and decoding improperly.
This is the javascript code above that I modified slightly to cross reference
const base64JS = require('js-base64');
const hmacSha256 = require('crypto-js/hmac-sha256');
const encBase64 = require('crypto-js/enc-base64');
function generateSignature(data) {
let signature = '';
const ts = "1569600658561"
const msg = base64JS.Base64.encode(data.apiKey + data.meetingNumber + ts + data.role);
console.log(msg); // This matches the ruby
const hash = hmacSha256(msg, data.apiSecret);
signature = base64JS.Base64.encodeURI(`${data.apiKey}.${data.meetingNumber}.${ts}.${data.role}.${encBase64.stringify(hash)}`);
return signature;
}
data = {
apiKey: 'api_key',
apiSecret: 'secret',
meetingNumber: '1000',
role: '0'
}
console.log(generateSignature(data));
This is my attempt in ruby
class ZoomSignatureGenerator
def self.generate
data = {
api_key: 'api_key',
api_secret: 'secret',
meeting_number: '1000',
role: '0'
}
ts = "1569600658561"
msg = Base64.encode64(data[:api_key] + data[:meeting_number] + ts + data[:role]);
puts(msg)
hash = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), data[:api_secret], msg)
signature = Base64.urlsafe_encode64("#{data[:api_key]}.#{data[:meeting_number]}.#{ts}.#{data[:role]}.#{Base64.encode64(hash)}");
return signature
end
end
I expected them to be the same output. But they end up being different.
Hope someone can help me :)
the output of Base64.encode64(value) is separated by a newline character, use Base64.strict_encode64(value) to generates the same result as Node.js.
require "base64"
require "openssl"
def generate_signature(api_key, api_secrete, meeting_number, role)
timestamp = 1544683367752
message = Base64.strict_encode64("#{api_key}#{meeting_number}#{timestamp}#{role}")
hash = Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', api_secrete, message))
signature = Base64.strict_encode64("#{api_key}.#{meeting_number}.#{timestamp}.#{role}.#{hash}")
end
generate_signature("key", "secret", 123456, 0)
Encountered the same problem, apparently Zoom uses base 64 encoded binary string, so instead of using HMAC.hexdigest try this
hmac = OpenSSL::HMAC.new(API_KEY, 'sha256')
hmac << data
hash = Base64.encode64(hmac.digest)
digest method returns the authentication code as a binary string,
hexdigest returns the authentication code as a hex-encoded string
Im trying to convert the C# code found here:
AMX Authorization Header in order to connect to an external API. The C# code works when trying to connect to the external API but when I convert it to a nodeJS solution it doesnt work.
I dont have access to the external C# API so can't update that side but was hoping someone could look at this and see something Im missing or doing wrong:
My nodejs solution:
var request = require('request');
var uuid = require('node-uuid');
var CryptoJS = require('crypto-js');
var URL = "https://urltoexternalAPI.com";
var itemAPPId = "testAPPId";
var APIKey = "testAPIKey";
var requestUri = encodeURIComponent(URL.toLowerCase());
var requestHttpMethod = "GET";
var requestTimeStamp = Math.floor(new Date().getTime() / 1000).toString();
var nonce = uuid.v1().replace(/-/g, '');
//I excluded the content hashing part as the API Im hitting is a GET request with no body content
var signatureRawData = itemAPPId + requestHttpMethod + requestUri + requestTimeStamp + nonce;
var secretKeyByteArray = CryptoJS.enc.Base64.parse(APIKey);
var signature = CryptoJS.enc.Utf8.parse(signatureRawData);
var signatureBytes = CryptoJS.HmacSHA256(signature, secretKeyByteArray);
var requestSignatureBase64String = signatureBytes.toString(CryptoJS.enc.Base64);
request({
url: URL,
headers: {
'Authorization': "amx "+itemAPPId+":"+requestSignatureBase64String+":"+nonce+":"+requestTimeStamp
}
}, function (error, response, body) {
if (response.statusCode != 200) {
console.log("Fail");
} else {
console.log("Success");
}
});
I figured it out! If anyone ever comes across this issue they may find the below helpful:
the following C# code works a little different to nodeJS:
System.Web.HttpUtility.UrlEncode(request.RequestUri.AbsoluteUri.ToLower());
Initially I copied this functionality as is and wrote the nodejs equivalent as such:
var requestUri = encodeURIComponent(URL.toLowerCase());
The encoding of the URL in C# keeps everything in lowercase - for e.g: https:// becomes https%3a%2f%2f - whereas nodeJS uppercases its encoding characters - https%3A%2F%2F - this is what as causing the incorrect hashing.
The solution is to just move the lowercase function to after the encoding has been done on the URL. Like so:
var requestUri = encodeURIComponent(URL).toLowerCase();
Seems rather simple but when trying to replicate the C# solution you may not pick up that the two URL encoders work differently.
Final solution: (updated to crypto thanks to Yoryo)
const fetch = require("node-fetch");
const uuid = require("uuid");
const crypto = require('crypto');
var URL = "https://urltoapi.com";
var itemAPPId = config.itemAPPId;
var APIKey = config.itemAPIKey;
var requestUri = encodeURIComponent(URL).toLowerCase();
var requestHttpMethod = "GET"; //should be dynamic
var requestTimeStamp = Math.floor(new Date().getTime() / 1000).toString();
var nonce = uuid.v1().replace(/-/g, '');
var signatureRawData = itemAPPId + requestHttpMethod + requestUri + requestTimeStamp + nonce;
var key = Buffer.from(APIKey, 'base64');
var requestSignatureBase64String = crypto.createHmac('sha256', key).update(signatureRawData, 'utf8').digest('base64');
const hitExternalAPI = async url => {
try {
const res = await fetch(url, { method: 'GET', headers: { "Authorization": "amx "+itemAPPId+":"+requestSignatureBase64String+":"+nonce+":"+requestTimeStamp } })
.then(res => {
console.log(res.ok);
});
} catch (error) {
console.log("Error",error);
}
};
hitExternalAPI(URL);
I would like to convert the following Node.JS code snippet to JavaScript in order to run it in Google Apps Script:
From: Node.JS
function getMessageSignature(path, request, nonce) {
var message = querystring.stringify(request);
var secret = new Buffer(config.secret, 'base64');
var hash = new crypto.createHash('sha256');
var hmac = new crypto.createHmac('sha512', secret);
var hash_digest = hash.update(nonce + message).digest('binary');
var hmac_digest = hmac.update(path + hash_digest, 'binary').digest('base64');
return hmac_digest;
}
This is the code I have tried so far (and many variations of it):
To: JavaScript / Google Apps Script
function getMessageSignature(url, request, nonce) {
// Message signature using HMAC-SHA512 of (URI path + SHA256(nonce + POST data))
//and base64 decoded secret API key
const secretApiKey = 'wdwdKswdKKewe23edeYIvL/GsltsGWbuBXnarcxZfu/9PjFbXl5npg==';
var secretApiKeyBytes = Utilities.base64Decode(secretApiKey);
var blob = Utilities.newBlob(secretApiKeyBytes);
var secretApiKeyString = blob.getDataAsString(); // POTENTIAL ERROR HERE?
var json = Utilities.jsonStringify(request);
var hash_digest = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256,
nonce + json);
var hmac_digest = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_512,
url + hash_digest, secretApiKeyString); // POTENTIAL ERROR HERE?
var base64 = Utilities.base64Encode(hmac_digest);
return base64;
}
When sending the signature as part of my request to the server, I always get the error message from the server: Invalid Key.
BTW: This is the API which I would like to use in JavaScript: Kraken API
I would appreciate any hint or suggestions very much!!
Solution:
Use jsSHA (https://github.com/Caligatio/jsSHA/) rather than Google App Script's functions. Create a new "jsSHA.gs" code file in Google App Script and copy/past in all the jsSHA optimised .js files from github.
function getKrakenSignature (path, postdata, nonce) {
var sha256obj = new jsSHA ("SHA-256", "BYTES");
sha256obj.update (nonce + postdata);
var hash_digest = sha256obj.getHash ("BYTES");
var sha512obj = new jsSHA ("SHA-512", "BYTES");
sha512obj.setHMACKey (api_key_private, "B64");
sha512obj.update (path);
sha512obj.update (hash_digest);
return sha512obj.getHMAC ("B64");
}
function getKrakenBalance () {
var path = "/0/private/Balance";
var nonce = new Date () * 1000;
var postdata = "nonce=" + nonce;
var signature = getKrakenSignature (path, postdata, nonce);
var url = api_url + path;
var options = {
method: 'post',
headers: {
'API-Key': api_key_public,
'API-Sign': signature
},
payload: postdata
};
var response = UrlFetchApp.fetch (url, options);
// ERROR handling
return response.getContentText ();
}
One problem is that querystring.stringify is not the same as Utilities.jsonStringify (which, FYI, is deprecated in favor of JSON.stringify).
I believe that this will be equivalent:
function queryStringify(obj) {
var params = [];
for(var key in obj) {
if(Object.hasOwnProperty(key)) {
if(typeof key === 'string') {
params.push([key, obj[key]]);
} else {
obj[key].forEach(function(val) {
params.push([key, val]);
});
}
}
}
return params.map(function(param) {
return encodeURIComponent(param[0]) + '=' + encodeURIComponent(param[1]);
}).join('&');
}
Though I am not sure if that is the reason you are seeing your error.
I noticed this nodejs to GS converter: https://www.npmjs.com/package/codegs
Haven't got the chance to use it, but it claims to handle 'require'-statements.