UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

286 lines (249 loc) 8.06 kB
import { createHash } from "crypto"; import { Protocols } from "../capabilities/Protocols"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; import { parseBitMask } from "../values/Primitive"; import { dskToString } from "./DSK"; import { SecurityClass } from "./SecurityClass"; function readNumber(qr: string, offset: number, length: number): number { return parseInt(qr.substr(offset, length), 10); } function fail(reason: string): never { throw new ZWaveError( `Invalid QR code: ${reason}`, ZWaveErrorCodes.Security2CC_InvalidQRCode, ); } /** Reads a number between 0 and 99 (2 decimal digits) */ function readLevel(qr: string, offset: number): number { const ret = readNumber(qr, offset, 2); if (ret > 99) fail("invalid data"); return ret; } /** Reads a byte (3 decimal digits) */ function readUInt8(qr: string, offset: number): number { const ret = readNumber(qr, offset, 3); if (ret > 0xff) fail("invalid data"); return ret; } /** Reads a 2-byte value (5 decimal digits) */ function readUInt16(qr: string, offset: number): number { const ret = readNumber(qr, offset, 5); if (ret > 0xffff) fail("invalid data"); return ret; } const onlyDigitsRegex = /^\d+$/; const minQRCodeLength = 52; // 2 digits Z, 2 digits version, 5 digits checksum, 3 digits keys, 40 digits DSK export enum QRCodeVersion { S2 = 0, SmartStart = 1, } export enum ProvisioningInformationType { ProductType = 0x00, ProductId = 0x01, MaxInclusionRequestInterval = 0x02, UUID16 = 0x03, SupportedProtocols = 0x04, // The ones below are NOT QR code compatible and therefore not implemented here Name = 0x32, Location = 0x33, SmartStartInclusionSetting = 0x34, AdvancedJoining = 0x35, BootstrappingMode = 0x36, NetworkStatus = 0x37, } export interface ProvisioningInformation_ProductType { genericDeviceClass: number; specificDeviceClass: number; installerIconType: number; } export interface ProvisioningInformation_ProductId { manufacturerId: number; productType: number; productId: number; applicationVersion: string; } export interface ProvisioningInformation_MaxInclusionRequestInterval { maxInclusionRequestInterval: number; } export interface ProvisioningInformation_UUID16 { uuid: string; } export interface ProvisioningInformation_SupportedProtocols { supportedProtocols: Protocols[]; } export type QRProvisioningInformation = { version: QRCodeVersion; /** * The security classes that were **requested** by the device. */ readonly requestedSecurityClasses: SecurityClass[]; /** * The security classes that will be **granted** to this device. * Until this has been changed by a user, this will be identical to {@link requestedSecurityClasses}. */ securityClasses: SecurityClass[]; dsk: string; } & ProvisioningInformation_ProductType & ProvisioningInformation_ProductId & Partial<ProvisioningInformation_MaxInclusionRequestInterval> & Partial<ProvisioningInformation_UUID16> & Partial<ProvisioningInformation_SupportedProtocols>; function parseTLVData(type: ProvisioningInformationType, data: string) { switch (type) { case ProvisioningInformationType.ProductType: { const deviceClasses = readUInt16(data, 0); const installerIconType = readUInt16(data, 5); const ret: ProvisioningInformation_ProductType = { genericDeviceClass: deviceClasses >>> 8, specificDeviceClass: deviceClasses & 0xff, installerIconType, }; return ret; } case ProvisioningInformationType.ProductId: { const manufacturerId = readUInt16(data, 0); const productType = readUInt16(data, 5); const productId = readUInt16(data, 10); const applicationVersionNumeric = readUInt16(data, 15); const applicationVersion = `${applicationVersionNumeric >>> 8}.${ applicationVersionNumeric & 0xff }`; const ret: ProvisioningInformation_ProductId = { manufacturerId, productType, productId, applicationVersion, }; return ret; } case ProvisioningInformationType.MaxInclusionRequestInterval: { const maxInclusionRequestInterval = 128 * readLevel(data, 0); const ret: ProvisioningInformation_MaxInclusionRequestInterval = { maxInclusionRequestInterval, }; return ret; } case ProvisioningInformationType.UUID16: { const buffer = Buffer.allocUnsafe(16); // Only format 0 is supported here const presentationFormat = readLevel(data, 0); if (presentationFormat !== 0) return; for (let chunk = 0; chunk < 8; chunk++) { const value = readUInt16(data, 2 + chunk * 5); buffer.writeUInt16BE(value, chunk * 2); } const ret: ProvisioningInformation_UUID16 = { uuid: buffer.toString("hex"), }; return ret; } case ProvisioningInformationType.SupportedProtocols: { const bitMask = Buffer.from([ data.length === 2 ? readLevel(data, 0) : data.length === 3 ? readUInt8(data, 0) : data.length === 5 ? readUInt16(data, 0) : 0, ]); const supportedProtocols = parseBitMask(bitMask, Protocols.ZWave); const ret: ProvisioningInformation_SupportedProtocols = { supportedProtocols, }; return ret; } } } function parseTLV(qr: string): { entry: { type: ProvisioningInformationType; } & Record<string, any>; charsRead: number; } { let offset = 0; if (qr.length - offset < 4) fail("incomplete TLV block"); const typeCritical = readLevel(qr, offset); const type = typeCritical >>> 1; const critical = !!(typeCritical & 0b1); const length = readLevel(qr, offset + 2); offset += 4; if (qr.length - offset < length) fail("incomplete TLV block"); const data = qr.substr(offset, length); offset += length; // Try to parse the raw data and fail if a critical block is not understood const parsed = parseTLVData(type, data); if (!parsed && critical) fail("Unsupported critical TLV block"); let entry: any; if (parsed) { entry = { type, ...parsed, }; } else { entry = { type, [ProvisioningInformationType[type]]: data, }; } return { entry, charsRead: offset, }; } /** Parses a string that has been decoded from a Z-Wave (S2 or SmartStart) QR code */ export function parseQRCodeString(qr: string): QRProvisioningInformation { if (!qr.startsWith("90")) fail("must start with 90"); if (qr.length < minQRCodeLength) fail("too short"); if (!onlyDigitsRegex.test(qr)) fail("contains invalid characters"); const version = readLevel(qr, 2); if (version > QRCodeVersion.SmartStart) fail("invalid version"); const checksum = readUInt16(qr, 4); // The checksum covers the remaining data const hash = createHash("sha1"); hash.update(Buffer.from(qr.substr(9), "ascii")); const expectedChecksum = hash.digest().readUInt16BE(0); if (checksum !== expectedChecksum) fail("invalid checksum"); const requestedKeysBitmask = readUInt8(qr, 9); const requestedSecurityClasses = parseBitMask( Buffer.from([requestedKeysBitmask]), SecurityClass.S2_Unauthenticated, ); if (!requestedSecurityClasses.every((k) => k in SecurityClass)) { fail("invalid security class requested"); } let offset = 12; const dsk = Buffer.allocUnsafe(16); for (let dskBlock = 0; dskBlock < 8; dskBlock++) { const block = readUInt16(qr, offset); dsk.writeUInt16BE(block, dskBlock * 2); offset += 5; } const ret = { version, // This seems like a duplication, but it's more convenient for applications to not have to copy this field over requestedSecurityClasses, securityClasses: [...requestedSecurityClasses], dsk: dskToString(dsk), } as QRProvisioningInformation; let hasProductID = false; let hasProductType = false; while (offset < qr.length) { const { entry: { type, ...data }, charsRead, } = parseTLV(qr.substr(offset)); offset += charsRead; if (type === ProvisioningInformationType.ProductId) { hasProductID = true; } else if (type === ProvisioningInformationType.ProductType) { hasProductType = true; } Object.assign(ret, data); } // Ensure the required fields are present if (!hasProductID || !hasProductType) { fail("missing required fields"); } return ret; }