Search code examples
javascriptapicryptographybinancebinance-api-client

How to produce valid signature at Binance API using Vanilla Javascript: {"code":-2014,"msg":"API-key format invalid."}


I've spent several hours trying to get the correct response from the endpoint. For that purpose I need to send a signature string generated by a SHA-256 function, attached to the query string I send to the server.

I've tried different methods to get that signature as described here in the docummentation: https://developers.binance.com/docs/binance-api/spot/index/#signed-trade-user_data-and-margin-endpoint-security But nothing seems to work.

I've tried several approaches for generating valid signatures using different libraries and functions, but they're not working (I even tried using the Web Crypto API documentation)

I'm getting this error when I make the call:

{"code":-2014,"msg":"API-key format invalid."}

This is the call:

https://testnet.binance.vision/api/v3/account?timestamp=my_timestamp&signature=my_signature

I guessed it was a problem with Fetch, but in other custom functions I have in my app it causes no problems.

Here's my code:

export async function getAccountInfo() {
    const apiSecret = pub.TESTNET_SECRETKEY; // Your secret key

    const timestamp = await serverTimestamp()
        .then(timestamp => {
            return timestamp;
        });

    let signature = sha256(apiSecret, timestamp);

    const testnet = 'https://testnet.binance.vision/api';

    // {{url}}/api/v3/account?timestamp={{timestamp}}&signature={{signature}}

    const fullUrl = testnet + '/v3/account?timestamp=' + timestamp + '&signature=' + signature;

    retrieveInformation(fullUrl);
}

Solution

  • I was just sending the incorrect timestamp string to the hashing function, in this line:

    let signature = sha256(apiSecret, timestamp); // Keep reading and you'll understand why.
    

    Vanilla JS Solution

    Though I used a dependency, I think this is still a complete valid solution.

    https://www.npmjs.com/package/jhash.js The functions are rather simple and straight forward to use.

    The Problem

    The problem was in the queryString I was sending to the hash function.

    As the Binance API documentation explains, though obscurely:

    • Endpoints use HMAC SHA256 signatures. The HMAC SHA256 signature is a keyed HMAC SHA256 operation. Use your secretKey as the key and totalParams as the value for the HMAC operation.
    • totalParams is defined as the query string concatenated with the request body.

    The last point really puzzled me.

    Now, the solution was to send the correct string (queryString) into the sha256 function. What it is required for the API is:

    https://testnet.binance.vision/api/v3/account?timestamp=my_timestamp&signature=my_signature
    

    The timestamp= substring was the solution for my problem. I had to send that tiny piece of code into the hex_hmac_sha256 function, which is the format required for the Binance API.

    The Complete Solution.

    async function serverTimestamp() {
        const url = 'https://testnet.binance.vision/api/v3/time';
        const timeServer = await getJson(url);
        return timeServer.serverTime;
    }
    

    Not the local time, but the time server must be sent inside the signature. This was the solution to the problem.

    export async function getAccountInfo() {
        const apiSecret = pub.TESTNET_SECRETKEY; // Your secret key
    
        const timestamp = await serverTimestamp()
            .then(timestamp => {
                return timestamp;
            });
    
        const queried_timestamp = 'timestamp=' + timestamp;
    
        // https://www.npmjs.com/package/jhash.js
        let signature = JHash.hex_hmac_sha256(apiSecret, queried_timestamp);
        // let signature = await sha256(apiSecret, queried_timestamp); // This one is not library dependant.
    
        const testnet = 'https://testnet.binance.vision/api';
    
        // {{url}}/api/v3/account?timestamp={{timestamp}}&signature={{signature}}
        const fullUrl = testnet + '/v3/account?timestamp=' + timestamp + '&signature=' + signature; // + '&recvWindow=60000';
        retrieveInformation(fullUrl);
    }
    

    Notice in the following line of code, I'm sending the string contained in the URL as a queryString.

    let signature = JHash.hex_hmac_sha256(apiSecret, queried_timestamp);
    // This is the same line than the one I wrote above,
    // but using another version of the function.
    

    This is the example that led me in the right direction: https://developers.binance.com/docs/binance-api/spot/index/#example-1-as-a-request-body

    As you can see in the official documentation example, they echoed the complete queryString(s) for making the signature.

    Now, the other functions you may need to understand better the problem:

    async function retrieveInformation(url = null) {
        const apiKey = pub.TESTNET_APIKEY; // Your ApiKey
        let httpHeaders = {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-MBX-APIKEY': apiKey
        }
    
        let myHeaders = new Headers(httpHeaders);
        var requestOptions = {
            headers: myHeaders
        };
        console.log(url);
        console.log(requestOptions);
        const data = await getJson(url, requestOptions);
        console.log(data);
        return data;
    }
    

    data is displayed as the following JSON object:

    {
      "makerCommission": 15,
      "takerCommission": 15,
      "buyerCommission": 0,
      "sellerCommission": 0,
      "canTrade": true,
      "canWithdraw": true,
      "canDeposit": true,
      "updateTime": 123456789,
      "accountType": "SPOT",
      "balances": [
        {
          "asset": "BTC",
          "free": "4723846.89208129",
          "locked": "0.00000000"
        },
        {
          "asset": "LTC",
          "free": "4763368.68006011",
          "locked": "0.00000000"
        }
      ],
      "permissions": [
        "SPOT"
      ]
    }
    

    You can see this same information shown here in the API Binance documentation: https://developers.binance.com/docs/binance-api/spot/index/#account-information-user_data

    Other functions I used (it's just a bonus to this answer, you may find them useful)

    Here's the fetch function I used:

    async function getJson(url = null, requestOptions = null) {
        return fetch(url, requestOptions)
            .then((response) => {
                if (!response.ok) {
                    throw Error(response.statusText);
                } else {
                    const jsoned = response.json();
                    return jsoned;
                    // NOTE:
                    //  response.json().then(data => {
                    //      → do something with your data
                    //  });
                    //
                }
            })
            .catch(function (error) {
                console.log(error);
            });
    }
    

    Here's the sha256 function I was able to make myself using some of the Mozilla documentation on the SubtleCrypto Object (Crypto Web API). It returns the same result than the one from the dependency.

    async function sha256(key, message) {
        
        // Step 1
        // encode as (utf-8) Uint8Array
        const msgUint8_key = new TextEncoder().encode(key);
        // encode as (utf-8) Uint8Array
        const msgUint8_message = new TextEncoder().encode(message);
        
        // Step 2
        const importedKey = await crypto.subtle.importKey('raw', msgUint8_key, {
            name: 'HMAC',
            hash: 'SHA-256'
        }, true, ['sign']);
        
        // Step 3
        const signedKey = await crypto.subtle.sign('HMAC', importedKey, msgUint8_message);
        // convert buffer to byte array
        const hashArray = Array.from(new Uint8Array(signedKey));
        // convert bytes to hex string    
        const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
        return hashHex;
    }
    

    For those looking for a more 100 % Vanilla solution to this last function: