Search code examples
javascriptreactjstuya

Stuck at 1004 - sign invalid from Tuya API


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
  };
}

Solution

  • 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.