Encryption for Smart Lock API
Hi,
I am running into an error when trying to create a temporary password for my Wi-Fi Smart Lock. This is my error message:
"Error: request api failed: param is illegal ,please check it"
Here are my steps:
- I retrieve a - ticket_keyand- ticket_idusing- /v1.0/devices/{device_id}/door-lock/password-ticketand receive a response:- Code: Select all - result: { expire_time: 47, ticket_id: 'w2mA7CbZ', ticket_key: 'EE2A3C241BB7F7C261EE9DC9025AA92BD2E5A66E1ADDC5A640D045C6F2B1E7CF' }, success: true, t: 1700684973862, tid: 'd8e56ec6897511ee9dcc3ea672c7e535' }
- I decrypt the - ticket_keywith my secret key using AES-ECB-128. The plaintext data is 64 bytes (a 256-bit key), such as- 98dfaf1dcd4a4beb8a733ae539635dddd6d4c9a1808cf5a37b3a56ffd86f5ddd
- I use this plaintext ticket_key to encrypt a 7 digit lock password, such as - 1234567. I guess it is AES-ECB-256 because the key I decrypted is 256-bits. Is this correct?
- I send POST request to - /v1.0/devices/{device_id}/door-lock/temp-password, with a body such as:- Code: Select all - { password: '9141A0E2AFEC0927A96685374B8ED7AA', password_type: 'ticket', ticket_id: 'w2mA7CbZ', effective_time: 1700934300, invalid_time: 1700984301, name: 'test1' }- I receive an error back: - Error: request api failed: param is illegal ,please check it
My code:
Code: Select all
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.tuyaus.com',
  /* fetch from openapi platform */
  accessKey: 'xxxxx',
  /* fetch from openapi platform */
  secretKey: 'xxxxx',
  /* Interface example device_ID */
  deviceId: 'xxxxx', // smart lock
};
const httpClient = axios.create({
  baseURL: config.host,
  timeout: 5 * 1e3,
});
async function main() {
  await getToken();
  const data = await createTemporaryPassword(config.deviceId,'1234567');
  console.log('fetch success: ', JSON.stringify(data,null,2));
}
/**
 * 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;
}
async function createTemporaryPassword(deviceId: string, password: string) {
  const tempData = await getLockTemporaryEncryptionKey(config.deviceId);
  const decryptedTempTicketKey = decrypt_AES_128(tempData.result.ticket_key,config.secretKey);
  const encryptedPassword = encrypt_AES_256(password,decryptedTempTicketKey.toString('hex')).toString('hex').toUpperCase();
  const method = 'POST';
  let url = `/v1.0/devices/${deviceId}/door-lock/temp-password`;
  const body = {
    "password":encryptedPassword,
    "password_type":"ticket",
    "ticket_id":tempData.result.ticket_id,
    "effective_time": 1700934300,
    "invalid_time":   1700984301,
    "name": "test1",
    //"phone":12345,
    //"time_zone":"",
  }
  const reqHeaders: { [k: string]: string } = await getRequestSign(url, method, {}, body);
  const { data } = await httpClient.request({
    method,
    data: body,
    params: {},
    headers: reqHeaders,
    url: reqHeaders.path,
  });
  console.log('Request:', method, reqHeaders.path, body);
  console.log('Response:', data);
  if (!data || !data.success) {
    throw Error(`request api failed: ${data.msg}`);
  }
  return data;
}
async function getLockTemporaryEncryptionKey(deviceId: string) {
  const method = 'POST';
  let url = `/v1.0/devices/${deviceId}/door-lock/password-ticket`;
  const reqHeaders: { [k: string]: string } = await getRequestSign(url, method);
  const body = {};
  const { data } = await httpClient.request({
    method,
    data: body,
    params: {},
    headers: reqHeaders,
    url: reqHeaders.path,
  });
  console.log('Request:', method, reqHeaders.path, body);
  console.log('Response:', data);
  if (!data || !data.success) {
    throw Error(`request api failed: ${data.msg}`);
  }
  return data;
}
/**
 * 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 query
 * @param body
 */
async function getRequestSign(
  path: string,
  method: string,
  query: { [k: string]: any } = {},
  body: { [k: string]: any } = {},
) {
  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 t = Date.now().toString();
  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}`);
});
// ECB encrypt 256 bit AES ECB
function encrypt_AES_256(data:string, secretKey:string) {
  const algorithm = 'aes-256-ecb'
  const buf = Buffer.from(data,'utf-8');
  const iv = null; // no IV for ECB cipher
  const key = Buffer.from(secretKey,'hex');
  const cipher = crypto.createCipheriv(algorithm, key, iv);
  cipher.setAutoPadding(true);
  const encrypted = Buffer.concat([cipher.update(buf), cipher.final()])
  return encrypted;
}
// ECB decrypt 128 bit AES ECB
function decrypt_AES_128(data:string, secretKey:string) {
  const algorithm = 'aes-128-ecb'
  const buf = Buffer.from(data,'hex');
  const iv = null; // no IV for ECB cipher
  const key = Buffer.from(secretKey,'hex');
  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  decipher.setAutoPadding(false);
  const decrypted = Buffer.concat([decipher.update(buf), decipher.final()])
  return decrypted;
}My POST body looks correct to me.  What am I doing wrong?
Please help, thank you!