import { enc, HmacSHA256, SHA256 } from 'crypto-js'; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { Client, Message, MQTTError } from 'paho-mqtt'; dayjs.extend(utc); // Helper functions to perform sigv4 operations function SigV4Utils() { } SigV4Utils.sign = function (key: CryptoJS.lib.WordArray, msg: string) { const hash = HmacSHA256(msg, key); return hash.toString(enc.Hex); }; SigV4Utils.sha256 = function (msg: string) { const hash = SHA256(msg); return hash.toString(enc.Hex); }; SigV4Utils.getSignatureKey = function (key: string, dateStamp: string, regionName: string, serviceName: string) { const kDate = HmacSHA256(dateStamp, 'AWS4' + key); const kRegion = HmacSHA256(regionName, kDate); const kService = HmacSHA256(serviceName, kRegion); const kSigning = HmacSHA256('aws4_request', kService); return kSigning; }; export const startSession = ({ endpoint, region, accessKeyId, secretAccessKey, sessionToken, clientId, }: { endpoint: string; region: string; accessKeyId: string; secretAccessKey: string; sessionToken: string; clientId: string; }, { onConnect, onFailure, onMessageArrived, onConnectionLost, }: { onConnect: () => void; onFailure: (e: MQTTError) => void; onMessageArrived: (m: Message) => void; onConnectionLost: (e: MQTTError) => void; }) => { // Get timestamp and format data const time = dayjs().utc(); const dateStamp = time.format('YYYYMMDD'); const amzdate = dateStamp + 'T' + time.format('HHmmss') + 'Z'; // Define constants used to create the message to be signed const service = 'iotdevicegateway'; const secretKey = secretAccessKey const accessKey = accessKeyId const algorithm = 'AWS4-HMAC-SHA256'; const method = 'GET'; const canonicalUri = '/mqtt'; const host = endpoint; // Set credential scope to today for a specific service in a specific region const credentialScope = dateStamp + '/' + region + '/' + service + '/' + 'aws4_request'; // Start populating the query string let canonicalQuerystring = 'X-Amz-Algorithm=AWS4-HMAC-SHA256'; // Add credential information canonicalQuerystring += '&X-Amz-Credential=' + encodeURIComponent(accessKey + '/' + credentialScope); // Add current date canonicalQuerystring += '&X-Amz-Date=' + amzdate; // Add expiry date // canonicalQuerystring += '&X-Amz-Expires=86400'; // Add headers, only using one = host canonicalQuerystring += '&X-Amz-SignedHeaders=host'; const canonicalHeaders = 'host:' + host + '\n'; // No payload, empty const payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; // empty string -> echo -n "" | xxd | shasum -a 256 // Build canonical request const canonicalRequest = method + '\n' + canonicalUri + '\n' + canonicalQuerystring + '\n' + canonicalHeaders + '\nhost\n' + payloadHash; // console.log('canonicalRequest: \n' + canonicalRequest); // Hash the canonical request and create the message to be signed const stringToSign = algorithm + '\n' + amzdate + '\n' + credentialScope + '\n' + SigV4Utils.sha256(canonicalRequest); // Derive the key to be used for the signature based on the scoped down request const signingKey = SigV4Utils.getSignatureKey(secretKey, dateStamp, region, service); // console.log('stringToSign: \n'); console.log(stringToSign); // console.log('signingKey: \n'); console.log(signingKey); // Calculate signature const signature = SigV4Utils.sign(signingKey, stringToSign); // Append signature to message canonicalQuerystring += '&X-Amz-Signature=' + signature; // Append existing security token to the request (since we are using STS credetials) or do nothing if using IAM credentials if (sessionToken !== "") { canonicalQuerystring += '&X-Amz-Security-Token=' + encodeURIComponent(sessionToken); } const requestUrl = 'wss://' + host + canonicalUri + '?' + canonicalQuerystring; // console.log(requestUrl); const mqtt_client = new Client(requestUrl, clientId); mqtt_client.onMessageArrived = onMessageArrived; mqtt_client.onConnectionLost = onConnectionLost; mqtt_client.connect({ onSuccess: onConnect, onFailure: onFailure, useSSL: true, timeout: 5, }); return mqtt_client; }