Keep getting Timestamp error on Coinbase API Call - authentication

I'm trying to connect my Google Sheet to Coinbase API using apps script. When I try to use the authentication with my keys, I keep getting the same error:
{"errors":[{"id":"authentication_error","message":"invalid timestamp"}]}
(Code 401).
I try to check the time difference between my request and Coinbase server (to see if it is more than 30 seconds) and it doesn't. (1589465439 (mine) / 1589465464 (server)).
My code:
var timestamp = Math.floor(Date.now() / 1000) + 15;
Logger.log(timestamp);
var req = {
method: 'GET',
path: '/v2/accounts',
body: ''
};
var message = timestamp + req.method + req.path + req.body;
var secret = Utilities.base64Decode(apiKey);
secret = Utilities.newBlob(secret).getDataAsString();
//create a hexedecimal encoded SHA256 signature of the message
var hmac = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256, message, secret);
var signature = Utilities.base64Encode(hmac);
Logger.log(signature);
var signatureStr = '';
for (i = 0; i < signature.length; i++) {
var byte = signature[i];
if (byte < 0)
byte += 256;
var byteStr = byte.toString(16);
// Ensure we have 2 chars in our byte, pad with 0
if (byteStr.length == 1) byteStr = '0' + byteStr;
signatureStr += byteStr;
}
Logger.log(signatureStr);
var options = {
baseUrl: 'https://api.coinbase.com/',
url: req.path,
method: req.method,
headers: {
'CB-ACCESS-SIGN': signatureStr,
'CB-ACCESS-TIMESTAMP': timestamp,
'CB-ACCESS-KEY': apiKey
}
};
var response = UrlFetchApp.fetch("https://api.coinbase.com/v2/accounts", options);

This is an old question, but in case it's still unsolved, I see a few changes that will fix this. I ran into a similar issue and had to solve it.
You should have 2 keys total (1 API key, 1 secret Key). Secret key is a separate key that comes from Coinbase, and is not a decoded variant of the API access key. It does not need explicit decoding.
Where you're passing timestamp as a header -> convert that value to a string to fix the timestamp error.
headers: {
'CB-ACCESS-SIGN': signatureStr,
'CB-ACCESS-TIMESTAMP': timestamp.toString(),
'CB-ACCESS-KEY': apiKey
}
You can collapse the hmac and signature variables into this one liner before converting to hex.
let signature = Utilities.computeHmacSha256Signature(message, secret);
These 3 changes make your code start to work with my keys.

Related

How to decrypt NodeJS crypto on the client side with a known encryption key?

I am trying to have AES encryption on the server side, and decryption on the client side. I have followed an example where CryptoJS is used on the client side for encryption and SubtleCrypto on the client side as well for decryption, but in my case I have the encryption and decryption separated.
Suppose I have the following encryption function within React Native:
const encrypt = (str: string) => {
const iv = crypto.randomBytes(12);
const myHexToken = "0x...."
const cipher = crypto.createCipheriv('aes-256-gcm', myHexToken.slice(0,32), iv)
let encrypted = cipher.update(str, 'utf8', 'hex')
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return {
message: encrypted,
tag: tag.toString('hex'),
iv: iv.toString('hex'),
};
};
This json is then posted to the client through a webview postMessage.
The client side has the following javascript injected:
var myHexToken = "0x....";
window.addEventListener("message", async function (event) {
var responseData = JSON.parse(event.data);
try {
var decryptedData = await decrypt(responseData.iv, responseData.message, responseData.tag);
} catch (e) {
alert(e);
}
// ...
How can I decrypt responseData.message within the WebView through SubtleCrypto of the Web Crypto API?
I have tried various things with the following methods, but I keep getting "OperationalError":
function fromHex(hexString) {
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}
function 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;
}
function fromBase64(base64String) {
return Uint8Array.from(window.atob(base64String), c => c.charCodeAt(0));
}
async function importKey(rawKey) {
var key = await crypto.subtle.importKey(
"raw",
rawKey,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
return key;
}
async function decrypt(iv, data, tag) {
var rawKey = fromHex(myHexToken.slice(0,32));
var iv = fromHex(iv);
var ciphertext = str2ab(data + tag);
var cryptoKey = await importKey(rawKey)
var decryptedData = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
cryptoKey,
ciphertext
)
var decoder = new TextDecoder();
var plaintext = decoder.decode(decryptedData);
return plaintext;
}
UPDATE 1: Added the getAuthTag implementation server side. Changed IV to have length of 12 bytes. Attempt to concatenate ciphertext and tag client side.
I have verified that "myHexToken" is the same both client and server side. Also, the return values of the server side "encrypt()" method are correctly sent to the client.
In the WebCrypto code the key must not be hex decoded with fromHex(), but must be converted to an ArrayBuffer with str2ab().
Also, the concatenation of ciphertext and tag must not be converted to an ArrayBuffer with str2ab(), but must be hex decoded with fromHex().
With these fixes decryption works:
Test:
For the test, the following hex encoded key and plaintext are used on the NodeJS side:
const myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff';
const plaintext = "The quick brown fox jumps over the lazy dog";
const encryptedData = encrypt(plaintext);
console.log(encryptedData);
This results e.g. in the following output:
{
message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
tag: '046c8e56bbd13db2faed82d1b19c665e',
iv: '11f87b0eaf006373ae8bc94d'
}
The ciphertext created this way can be successfully decrypted with the fixed JavaScript code:
(async () => {
function fromHex(hexString) {
return new Uint8Array(hexString.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
}
function 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;
}
async function importKey(rawKey) {
var key = await crypto.subtle.importKey(
"raw",
rawKey,
"AES-GCM",
true,
["encrypt", "decrypt"]
);
return key;
}
async function decrypt(iv, data, tag) {
//var rawKey = fromHex(myHexToken.slice(0,32)); // Fix 1
var rawKey = str2ab(myHexToken.slice(0,32));
var iv = fromHex(iv);
//var ciphertext = str2ab(data + tag); // Fix 2
var ciphertext = fromHex(data + tag);
var cryptoKey = await importKey(rawKey)
var decryptedData = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv
},
cryptoKey,
ciphertext
);
var decoder = new TextDecoder();
var plaintext = decoder.decode(decryptedData);
return plaintext;
}
var myHexToken = '000102030405060708090a0b0c0d0e0ff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'
var data = {
message: 'cc4beae785cda5c9413f49cf9449a6ae17fdc0f7435b9a8fd954602bdb4f4b825793f6b561c0d9a709007c',
tag: '046c8e56bbd13db2faed82d1b19c665e',
iv: '11f87b0eaf006373ae8bc94d'
}
var plaintext = await decrypt(data.iv, data.message, data.tag);
console.log(plaintext);
})();
A remark about the key: In the posted NodeJS code, const myHexToken = "0x...." is set. It's not clear to me if the 0x prefix is just supposed to symbolize a hex encoded string, or is really contained in the string. If the latter, it should actually be removed before the implicit UTF-8 encoding (by createCiperiv()). In case of a hex decoding it must be removed anyway.
In the posted example a valid hex encoded 32 bytes key is used (i.e. without 0x prefix).
With regard to the key encoding, also note the following:
The conversion of the key from a hex encoded string by a UTF-8 (or ASCII) encoding results in only half of the key being considered, in the example: 000102030405060708090a0b0c0d0e0f. This reduces security, because the value range per byte is reduced from 256 to 16 values.
In order for the entire key to be considered, the correct conversion on the NodeJS side would be: Buffer.from(myHexToken, 'hex') and on the WebCrypto side: var rawKey = fromHex(myHexToken).
Because of its implicit UTF8 encoding crypto.createCipheriv(..., myHexToken.slice(0,32), ...) creates a 32 bytes key and is functionally identical to str2ab(myHexToken.slice(0,32)) only as long as the characters in the substring myHexToken.slice(0,32) correspond to ASCII characters (which is true for a hex encoded string).

How can I get data from Bitfinex authenticated api endpoints using Google sheets?

Using Google sheets, I have stored my api_key and api_secret in the Property service section of user info as respectively "api_key" and api_secret".
I want to get wallet info from my account. The code I have written is as follows:
function wallet() {
var api_key = PropertiesService.getScriptProperties().getProperty('api_key');
var api_secret = PropertiesService.getScriptProperties().getProperty('api_secret');
var response = UrlFetchApp.fetch("https://api.bitfinex.com/v2/auth/r/wallets", api_key, api_secret);
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("sheet");
var result = JSON.parse(response.getContentText());
var wallet_btc = result.BALANCE
}
When I run in debug mode the error message is:
Cannot find method fetch(string,null,null). (line 13, file "Code")
Is this approach wrong, the code wrong, or both?
Many thanks.
How about the following modifications?
Modification points :
The parameters for UrlFetchApp.fetch() are UrlFetchApp.fetch(url, params). And params is an object.
This is the reason of error Cannot find method fetch(string,null,null). (line 13, file "Code").
When I saw the sample scripts for Bitfinex API, the request body has to be created using api_key, api_secret, nonce, body and signature. And signature is encrypted by HMAC_SHA_384 and converted to the string of the unsigned hexadecimal.
The sample for the endpoint of https://api.bitfinex.com/v2/auth/r/wallets is as follows. This is from API reference.
Sample for the endpoint of https://api.bitfinex.com/v2/auth/r/wallets
request.post(
`${url}/auth/r/wallets`,
headers: { /* auth headers */ },
body: {},
json: true,
(error, response, body) => console.log(body)
)
When above points are reflected to your script, the modified script is as follows.
Modified script :
function wallet() {
var api_key = PropertiesService.getScriptProperties().getProperty('api_key');
var api_secret = PropertiesService.getScriptProperties().getProperty('api_secret');
var apiPath = "v2/auth/r/wallets";
var nonce = Date.now().toString();
var body = {};
var rawBody = JSON.stringify(body);
var signature = "/api/" + apiPath + nonce + rawBody;
signature = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_384, signature, api_secret)
.map(function(e) {
var v = (e < 0 ? e + 256 : e).toString(16);
return v.length == 1 ? "0" + v : v;
}).join("");
var url = "https://api.bitfinex.com/" + apiPath;
var options = {
method: 'POST',
contentType: "application/json",
headers: {
'bfx-nonce': nonce,
'bfx-apikey': api_key,
'bfx-signature': signature
},
payload: rawBody
};
var response = UrlFetchApp.fetch(url, options);
var result = JSON.parse(response.getContentText());
Logger.log(result)
// var wallet_btc = result.BALANCE // I couldn't confirm whether this key exists.
}
References :
Sample scripts for Bitfinex API
API reference
UrlFetchApp.fetch()
I cannot confirm whether this works. If this didn't work, can you tell me the situation? I would like to modify.
Edit :
When you want 0.0957596 from the result of [["exchange", "USD", 14.81076629, 0, null], ["exchange", "BTC", 0.0957596, 0, null], ["funding", "BTC", 4.13E-6, 0, null], ["funding", "ETH", 3.50186961, 0, null], ["exchange", "OMG", 5.9E-7, 0, null]];, you can use the following script.
Script :
function wallet() {
var api_key = PropertiesService.getScriptProperties().getProperty('api_key');
var api_secret = PropertiesService.getScriptProperties().getProperty('api_secret');
var apiPath = "v2/auth/r/wallets";
var nonce = Date.now().toString();
var body = {};
var rawBody = JSON.stringify(body);
var signature = "/api/" + apiPath + nonce + rawBody;
signature = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_384, signature, api_secret)
.map(function(e) {
var v = (e < 0 ? e + 256 : e).toString(16);
return v.length == 1 ? "0" + v : v;
}).join("");
var url = "https://api.bitfinex.com/" + apiPath;
var options = {
method: 'POST',
contentType: "application/json",
headers: {
'bfx-nonce': nonce,
'bfx-apikey': api_key,
'bfx-signature': signature
},
payload: rawBody
};
var response = UrlFetchApp.fetch(url, options);
var result = JSON.parse(response.getContentText());
// Logger.log(result)
// var wallet_btc = result.BALANCE // I couldn't confirm whether this key exists.
var balance = 0;
for (var i in result) {
if (result[i][0] == "exchange" && result[i][1] == "BTC") {
balance = result[i][2];
break;
}
}
Logger.log(balance)
}
Note :
From the document, it seems that the indexes of WALLET_TYPE, CURRENCY and BALANCE are always 0, 1 and 2 of each element in the response, respectively.

Cryptopia API in Google Sheets (Google Apps Script)

In continuation of building Google SpreadSheet using Google Apps Script I've done with getting my Bittrex and Poloniex balances, but can't get to work with Cryptopia.
Here is a link to my struggles with Bittrex Map JSON objects array to strings
Here is an official API links: https://www.cryptopia.co.nz/Forum/Thread/256
Here are some examples:
https://www.cryptopia.co.nz/Forum/Thread/262
https://github.com/Coac/cryptopia.js/blob/master/index.js
https://github.com/sigwo/node-cryptopia/blob/master/cryptopia.js
Here is my code, which getting "Invalid authorization header" error:
// Get Cryptopia balances
var key = keys.getRange("B4").getValue();
var secret = keys.getRange("C4").getValue();
var baseUrl = 'https://www.cryptopia.co.nz/api/';
var command = "GetBalance";
var url = baseUrl + command;
var signature = key + "POST" + encodeURIComponent(url).toLowerCase() + nonce;
var hmacsignature = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256,signature,secret);
var header_value = "amx " + key + ":" + hmacsignature + ":" + nonce;
var headers = { 'Authorization': header_value, 'Content-Type':'application/json; charset=utf-8' };
var options = {
method: 'POST',
headers: headers
};
var response = UrlFetchApp.fetch("https://www.cryptopia.co.nz/api/GetBalance", options);
var json = JSON.parse(response.getContentText());
}
From your sample links, it seems that hmacsignature is encoded by base64. So how about the following modofication?
From :
var hmacsignature = Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256,signature,secret);
To :
var hmacsignature = Utilities.base64Encode(Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256,signature,secret));
Note :
Is nonce declared? If you have not declared it, you can use a following script.
var nonce = Math.floor(new Date().getTime() / 1000);
I cannot test this. So I don't know whether this works fine. If this didn't work, I'm sorry.
Edit :
How about this? var params = {}; might be required, even if there is no request parameters. So I added this and Content-Length. And then, is secret encoded by base64? I thought that it may be encoded from other scripts.
var key = keys.getRange("B4").getValue();
var secret = keys.getRange("C4").getValue();
var nonce = Math.floor(new Date().getTime() / 1000); // Added
var params = {}; // Added
var baseUrl = 'https://www.cryptopia.co.nz/api/';
var command = "GetBalance";
var url = baseUrl + command;
var requestContentBase64String = Utilities.base64Encode(Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, JSON.stringify(params), Utilities.Charset.UTF_8)); // Added
var signature = key + "POST" + encodeURIComponent(url).toLowerCase() + nonce + requestContentBase64String; // Modified
var hmacsignature = Utilities.base64Encode(Utilities.computeHmacSignature(Utilities.MacAlgorithm.HMAC_SHA_256, signature, Utilities.base64Decode(secret), Utilities.Charset.UTF_8)); // Modified
var header_value = "amx " + key + ":" + hmacsignature + ":" + nonce;
var headers = {
"Authorization": header_value,
"Content-Type": 'application/json; charset=utf-8',
"Content-Length" : Utilities.newBlob(JSON.stringify(params)).getBytes().length // Added
};
var options = {
method: 'POST',
headers: headers
};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
Utilities.computeHmacSignature(algorithm, value, key) method does not compute the binary input correctly. The type of value and key parameter is String, but Utilities.base64Decode's result is Byte[]. The raw binary value is changed while being converted from Byte[] to String.
Use jsSHA, and cf. https://stackoverflow.com/a/14007167.
The following can be used to calculate the correct value, and fetch the result by proper UrlFetchApp.fetch's options.
.... paste src/sha256.js contents ...
...
var params = {"Currency" : "BTC"};
...
var sha = new jsSHA("SHA-256", "TEXT");
sha.setHMACKey(secret, "B64");
sha.update(signature);
var hmacsignature = sha.getHMAC("B64");
var header_value = "amx " + key + ":" + hmacsignature + ":" + nonce;
var headers = {
"Authorization" : header_value,
};
var options = {
"contentType": 'application/json; charset=utf-8',
"method": 'post',
"headers": headers,
"payload": JSON.stringify(params),
"contentLength": JSON.stringify(params).length
};
var response = UrlFetchApp.fetch(url, options);
var json = JSON.parse(response.getContentText());
Logger.log(json);

Authentication error in twitter login

I am facing some authentication error using Twitter Rest APis. Although it is working on Postman.On postman there is an option of adding Consumer Secret in Authorization but I don't understand where to put that key in my URLRequest. I am using same code snippet as of Postman but on my app side i face following auth error.
let requestTokenURL = URL(string:"https://api.twitter.com/oauth/request_token")
let consumerKey = "xxxxxxxxxxxxxxxxxxxxxx"
let consumerSecretKey = "xxxxxxxxxxxxxxxxx"
let signatureMethod = "HMAC-SHA1"
let signature = "hgjhagdAGFSSAJKaqhsugqggqskugkg"
let timestamp = String(Date().timeIntervalSince1970)
let nonce = UUID().uuidString
let version = "1.0"
class TwitterHelper{
func getAuthToken(){
let session = URLSession.shared
let info = [["OAuth oauth_consumer_key":consumerKey],
["oauth_signature_method":signatureMethod],
["oauth_timestamp":timestamp],
["oauth_nonce":nonce],
["oauth_version":"1.0"],
["oauth_signature":signature]
]
var formattedString = ""
for case let authData in info {
for (key,value) in authData{
formattedString += key + "=" + (value) + ","
}
}
let headers = [
"content-type": "application/x-www-form-urlencoded",
"Authorization":formattedString
]
var request = URLRequest(url: requestTokenURL!, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
session.dataTask(with: request) { (data, response, error) in
if let parsedData = parseJsonData(data: data){
print(parsedData)
}
}.resume()
}
}
Response:
{
code = 32;
message = "Could not authenticate you.";
}
It is due to the consumer secret key. In twitter documentation it is not defined to use but when I use it in Postman Authorization , it works. I don't understand how to use that "consumer secret key" in header.
You don't require the Consumer Secret for oauth 1.0, which is what you are trying to do in your code (just for oauth 2).
The issue is most likely around the oauth signature you are creating as this is quite involved and tricky to get right.
Make sure you are following all the steps outlined here to create the signature base. Remember also to convert your consumer key to utf8 and then base64 encode it too.

PhoneGap FileTransfer with HTTP basic authentication

I'm attempting to upload a file from PhoneGap to a server using the FileTransfer method. I need HTTP basic auth to be enabled for this upload.
Here's the relevant code:
var options = new FileUploadOptions({
fileKey: "file",
params: {
id: my_id,
headers: { 'Authorization': _make_authstr() }
}
});
var ft = new FileTransfer();
ft.upload(image, 'http://locahost:8000/api/upload', success, error, options);
Looking over the PhoneGap source code it appears that I can specify the authorization header by including "headers" in the "params" list as I've done above:
JSONObject headers = params.getJSONObject("headers");
for (Iterator iter = headers.keys(); iter.hasNext();)
{
String headerKey = iter.next().toString();
conn.setRequestProperty(headerKey, headers.getString(headerKey));
}
However, this doesn't seem to actually add the header.
So: is there a way to do HTTP basic auth with PhoneGap's FileTransfer, for both iPhone and Android?
You can add custom headers by adding them to the options rather than the params like so:
authHeaderValue = function(username, password) {
var tok = username + ':' + password;
var hash = btoa(tok);
return "Basic " + hash;
};
options.headers = {'Authorization': authHeaderValue('Bob', '1234') };
The correct location for the headers array is as an immediate child of options. options->headers. Not options->params->headers. Here is an example:
//**************************************************************
//Variables used below:
//1 - image_name: contains the actual name of the image file.
//2 - token: contains authorization token. In my case, JWT.
//3 - UPLOAD_URL: URL to which the file will be uploaded.
//4 - image_full_path - Full path for the picture to be uploaded.
//***************************************************************
var options = {
fileKey: "file",
fileName: 'picture',
chunkedMode: false,
mimeType: "multipart/form-data",
params : {'fileName': image_name}
};
var headers = {'Authorization':token};
//Here is the magic!
options.headers = headers;
//NOTE: I creaed a separate object for headers to better exemplify what
// is going on here. Obviously you can simply add the header entry
// directly to options object above.
$cordovaFileTransfer.upload(UPLOAD_URL, image_full_path, options).then(
function(result) {
//do whatever with the result here.
});
Here is the official documentation: https://github.com/apache/cordova-plugin-file-transfer
You can create a authorization header yourself. But you can also enter the credentials in the url like this:
var username = "test", password = "pass";
var uri = encodeURI("http://"+username + ':' + password +"#localhost:8000/api/upload");
See FileTransfer.js for the implementation (line 45):
function getBasicAuthHeader(urlString) {
var header = null;
// This is changed due to MS Windows doesn't support credentials in http uris
// so we detect them by regexp and strip off from result url
// Proof: http://social.msdn.microsoft.com/Forums/windowsapps/en-US/a327cf3c-f033-4a54-8b7f-03c56ba3203f/windows-foundation-uri-security-problem
if (window.btoa) {
var credentials = getUrlCredentials(urlString);
if (credentials) {
var authHeader = "Authorization";
var authHeaderValue = "Basic " + window.btoa(credentials);
header = {
name : authHeader,
value : authHeaderValue
};
}
}
return header;
}