Stuck at 1004 - sign invalid from Tuya API

2.9k Views Asked by At

I tried controlling my GoSund smart socket using Tuya IoT Development Platform, but I'm stuck on this error response when trying to switch its state:

{"code":1004,"msg":"sign invalid","success":false,"t":1658384161392,"tid":"97e938e608bc11eda4f0322e56e3d437"}

The following code is basically slightly modified copy of develop code sample from official Tuya API site with my keys and deviceId pasted(https://developer.tuya.com/en/docs/iot/singnature?id=Ka43a5mtx1gsc)

When I tried to do the exact same thing using Tuya's site debug device option it just works. When I try to do it using their code sample in a web app, it fails with 1004. Except for the token that is new every time I call this, basically all the request headers are the same as when calling them from Tuya's site. Payload is the same too, but the response is very different.

same request on Tuya website device debugging & in a web app

Adding sign_version: '2.0' to request headers or using different url (const url = /v1.0/iot-03/devices/${deviceId}/commands;) doesn't seem to help.

const config = {
  /* openapi host */
  //host: 'https://openapi.tuyacn.com',
  host: 'https://openapi.tuyaeu.com',
  /* fetch from openapi platform */
  accessKey: 'I pasted here my Access ID/Client ID from iot.tuya.com',
  /* fetch from openapi platform */
  secretKey: 'I pasted here my Access Secret/Client Secret from iot.tuya.com',
  /* Interface example device_ID */
  deviceId: 'I pasted here Device ID of my GoSund smart plug',
};

const httpClient = axios.create({
  baseURL: config.host,
  timeout: 5 * 1e3,
});

async main(switchValue: boolean) {
  try{
    await this.getToken();
    const data = await this.getDeviceInfo(config.deviceId, switchValue);
    console.log('fetch success: ', JSON.stringify(data));
  }catch(error){
    console.log(error);
  }
}

/**
 * fetch highway login token
 */
async getToken() {
  const method = 'GET';
  const timestamp = Date.now().toString();
  const signUrl = '/v1.0/token?grant_type=1';
  const contentHash = crypto.createHash('sha256').update('').digest('hex');
  const stringToSign = [method, contentHash, '', signUrl].join('\n');
  const signStr = config.accessKey + timestamp + stringToSign;

  const headers = {
    t: timestamp,
    sign_method: 'HMAC-SHA256',
    client_id: config.accessKey,
    sign: await this.encryptStr(signStr, config.secretKey),
  };
  const { data: login } = await httpClient.get('/v1.0/token?grant_type=1', { headers });
  if (!login || !login.success) {
    throw Error(`fetch failed: ${login.msg}`);
  }
  this.setState({ token: login.result.access_token })
}

/**
 * fetch highway business data
 */
async getDeviceInfo(deviceId: string, switchValue: boolean) {
  const query = {};
  const method = 'POST';
  const url = `/v1.0/devices/${deviceId}/commands`;
  const reqHeaders: { [k: string]: string } = await this.getRequestSign(url, method, {}, query);

  const { data } = await httpClient.request({
    method,
    data: {commands: [{code: "countdown_1", value: 0}, {code: "switch", value: switchValue}]},
    params: {},
    headers: reqHeaders,
    url: reqHeaders.path,
  });
  if (!data || !data.success) {
    throw Error(`request api failed: ${data.msg}`);
  }
}
/**
 * HMAC-SHA256 crypto function
 */
async encryptStr(str: string, secret: string): Promise<string> {
  return crypto.createHmac('sha256', secret).update(str, 'utf8').digest('hex').toUpperCase();
}

/**
 * request sign, save headers 
 * @param path
 * @param method
 * @param headers
 * @param query
 * @param body
 */
async getRequestSign(
  path: string,
  method: string,
  headers: { [k: string]: string } = {},
  query: { [k: string]: any } = {},
  body: { [k: string]: any } = {},
) {
  const t = Date.now().toString();
  const [uri, pathQuery] = path.split('?');
  const queryMerged = Object.assign(query, qs.parse(pathQuery));
  const sortedQuery: { [k: string]: string } = {};
  Object.keys(queryMerged)
    .sort()
    .forEach((i) => (sortedQuery[i] = query[i]));

  const querystring = decodeURIComponent(qs.stringify(sortedQuery));
  const url = querystring ? `${uri}?${querystring}` : uri;
  const contentHash = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
  const client_id = config.accessKey
  const access_token = this.state.token

  const stringToSign = [method, contentHash, '', url].join('\n');
  const signStr = client_id + access_token + t + stringToSign;
  return {
    t,
    path: url,
    client_id: config.accessKey,
    sign: await this.encryptStr(signStr, config.secretKey),
    sign_method: 'HMAC-SHA256',
    sign_version: '2.0',
    access_token: access_token
  };
}
2

There are 2 best solutions below

2
On BEST ANSWER

Looks like you're not passing the body to the signature method. The whole request needs to be signed including any body. You can't change the request details after signing it, except to add the sign header.

It's probably worth structuring your call into three steps - one to build up the request object. One to add the signing header based on the whole request object (so it's responsible for signing the right fields). Then finally send it to httpClient.request to make the call.

I presume there's a bit of left over "trying things out to get it working" in your code, e.g. setting the url to the requestHeaders.path. And I think you need a timestamp header in there too. All should be in the docu, or look at Tuya's postman collection's pre-request script.

0
On

Their example script has a few errors:

In getDeviceInfo():

  • set method to GET instead of POST
  • set url to "/v1.0/iot-03/devices/${deviceId}/functions" or "/v1.0/iot-03/devices/${deviceId}/specification"
  • return data; at the end, so it gets output

This made it work for me.

Ah, and this answer relates to today's version of their example:

import * as qs from 'qs';
import * as crypto from 'crypto';
import { default as axios } from 'axios';

let token = '';

const config = {
  /* openapi host */
  host: 'https://openapi.tuyacn.com',
  /* fetch from openapi platform */
  accessKey: '',
  /* fetch from openapi platform */
  secretKey: '',
  /* Interface example device_ID */
  deviceId: '',
};

const httpClient = axios.create({
  baseURL: config.host,
  timeout: 5 * 1e3,
});

async function main() {
  await getToken();
  const data = await getDeviceInfo(config.deviceId);
  console.log('fetch success: ', JSON.stringify(data));
}

/**
 * fetch highway login token
 */
async function getToken() {
  const method = 'GET';
  const timestamp = Date.now().toString();
  const signUrl = '/v1.0/token?grant_type=1';
  const contentHash = crypto.createHash('sha256').update('').digest('hex');
  const stringToSign = [method, contentHash, '', signUrl].join('\n');
  const signStr = config.accessKey + timestamp + stringToSign;

  const headers = {
    t: timestamp,
    sign_method: 'HMAC-SHA256',
    client_id: config.accessKey,
    sign: await encryptStr(signStr, config.secretKey),
  };
  const { data: login } = await httpClient.get('/v1.0/token?grant_type=1', { headers });
  if (!login || !login.success) {
    throw Error(`fetch failed: ${login.msg}`);
  }
  token = login.result.access_token;
}

/**
 * fetch highway business data
 */
async function getDeviceInfo(deviceId: string) {
  const query = {};
  const method = 'POST';
  const url = `/v1.0/devices/${deviceId}/commands`;
  const reqHeaders: { [k: string]: string } = await getRequestSign(url, method, {}, query);

  const { data } = await httpClient.request({
    method,
    data: {},
    params: {},
    headers: reqHeaders,
    url: reqHeaders.path,
  });
  if (!data || !data.success) {
    throw Error(`request api failed: ${data.msg}`);
  }
}

/**
 * HMAC-SHA256 crypto function
 */
async function encryptStr(str: string, secret: string): Promise<string> {
  return crypto.createHmac('sha256', secret).update(str, 'utf8').digest('hex').toUpperCase();
}

/**
 * request sign, save headers 
 * @param path
 * @param method
 * @param headers
 * @param query
 * @param body
 */
async function getRequestSign(
  path: string,
  method: string,
  headers: { [k: string]: string } = {},
  query: { [k: string]: any } = {},
  body: { [k: string]: any } = {},
) {
  const t = Date.now().toString();
  const [uri, pathQuery] = path.split('?');
  const queryMerged = Object.assign(query, qs.parse(pathQuery));
  const sortedQuery: { [k: string]: string } = {};
  Object.keys(queryMerged)
    .sort()
    .forEach((i) => (sortedQuery[i] = query[i]));

  const querystring = decodeURIComponent(qs.stringify(sortedQuery));
  const url = querystring ? `${uri}?${querystring}` : uri;
  const contentHash = crypto.createHash('sha256').update(JSON.stringify(body)).digest('hex');
  const stringToSign = [method, contentHash, '', url].join('\n');
  const signStr = config.accessKey + token + t + stringToSign;
  return {
    t,
    path: url,
    client_id: config.accessKey,
    sign: await encryptStr(signStr, config.secretKey),
    sign_method: 'HMAC-SHA256',
    access_token: token,
  };
}


main().catch(err => {
  throw Error(`error: ${err}`);
});