UNPKG

hap-controller

Version:

Library to implement a HAP (HomeKit) controller

777 lines 33 kB
"use strict"; /** * Build and parse packets for pairing protocol requests. */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.PairingTypeFlags = exports.PairMethods = exports.Types = exports.ErrorCodes = void 0; const tlv_1 = require("../model/tlv"); const libsodium_wrappers_1 = __importDefault(require("libsodium-wrappers")); const uuid_1 = require("uuid"); const fast_srp_hap_1 = require("fast-srp-hap"); const node_hkdf_sync_1 = __importDefault(require("node-hkdf-sync")); const error_1 = __importDefault(require("../model/error")); const Steps = { M1: 1, M2: 2, M3: 3, M4: 4, M5: 5, M6: 6, }; /** * See Table 5-3, Table 7-38 */ const Methods = { PairSetup: 0, PairSetupWithAuth: 1, PairVerify: 2, AddPairing: 3, RemovePairing: 4, ListPairings: 5, PairResume: 6, }; const PairMethods = { PairSetup: Methods.PairSetup, PairSetupWithAuth: Methods.PairSetupWithAuth, }; exports.PairMethods = PairMethods; /** * See Table 5-5 */ exports.ErrorCodes = { kTLVError_Unknown: 0x01, kTLVError_Authentication: 0x02, kTLVError_Backoff: 0x03, kTLVError_MaxPeers: 0x04, kTLVError_MaxTries: 0x05, kTLVError_Unavailable: 0x06, kTLVError_Busy: 0x07, }; /** * See Table 5-6, Table 7-38 */ exports.Types = { kTLVType_Method: 0x00, kTLVType_Identifier: 0x01, kTLVType_Salt: 0x02, kTLVType_PublicKey: 0x03, kTLVType_Proof: 0x04, kTLVType_EncryptedData: 0x05, kTLVType_State: 0x06, kTLVType_Error: 0x07, kTLVType_RetryDelay: 0x08, kTLVType_Certificate: 0x09, kTLVType_Signature: 0x0a, kTLVType_Permissions: 0x0b, kTLVType_FragmentData: 0x0c, kTLVType_FragmentLast: 0x0d, kTLVType_SessionID: 0x0e, kTLVType_Flags: 0x13, kTLVType_Separator: 0xff, }; /** * See Table 5-7 */ const PairingTypeFlags = { kPairingFlag_Transient: 0x00000010, kPairingFlag_Split: 0x01000000, }; exports.PairingTypeFlags = PairingTypeFlags; class PairingProtocol { /** * Create the PairingProtocol object. * * @param {Object?} pairingData - Optional saved pairing data */ constructor(pairingData) { this.AccessoryPairingID = null; if (pairingData === null || pairingData === void 0 ? void 0 : pairingData.AccessoryPairingID) { this.AccessoryPairingID = PairingProtocol.bufferFromHex(pairingData.AccessoryPairingID); } this.AccessoryLTPK = null; if (pairingData === null || pairingData === void 0 ? void 0 : pairingData.AccessoryLTPK) { this.AccessoryLTPK = PairingProtocol.bufferFromHex(pairingData.AccessoryLTPK); } this.iOSDevicePairingID = null; if (pairingData === null || pairingData === void 0 ? void 0 : pairingData.iOSDevicePairingID) { this.iOSDevicePairingID = PairingProtocol.bufferFromHex(pairingData.iOSDevicePairingID); } this.iOSDeviceLTSK = null; if (pairingData === null || pairingData === void 0 ? void 0 : pairingData.iOSDeviceLTSK) { this.iOSDeviceLTSK = PairingProtocol.bufferFromHex(pairingData.iOSDeviceLTSK); } this.iOSDeviceLTPK = null; if (pairingData === null || pairingData === void 0 ? void 0 : pairingData.iOSDeviceLTPK) { this.iOSDeviceLTPK = PairingProtocol.bufferFromHex(pairingData.iOSDeviceLTPK); } this.srpClient = null; this.pairSetup = { sessionKey: null, pairMethod: null, pairTypeFlags: 0, }; this.pairVerify = { privateKey: null, publicKey: null, sessionKey: null, sharedSecret: null, accessoryPublicKey: null, sessionID: null, }; this.sessionKeys = { accessoryToControllerKey: null, controllerToAccessoryKey: null, }; } /** * Parse a buffer from a hex string. */ static bufferFromHex(buf) { if (typeof buf === 'string') { return Buffer.from(buf, 'hex'); } return buf; } /** * Convert a buffer to a hex string. */ static bufferToHex(buf) { if (buf) { return buf.toString('hex'); } return buf; } /** * Determine whether or not we can use pair resume. * * @returns {boolean} Boolean indicating if pair resume is possible. */ canResume() { return Buffer.isBuffer(this.pairVerify.sessionID); } /** * Return info if the current pairSetup process is Transient only * * @returns {boolean} Boolean indicating if the pairSetup process has only the transient flag set */ isTransientOnlyPairSetup() { if (this.pairSetup.pairMethod !== PairMethods.PairSetup) { return false; } return this.pairSetup.pairTypeFlags === PairingTypeFlags.kPairingFlag_Transient; } /** * Verify the provided PIN * * @param pin {string} PIN */ verifyPin(pin) { const re = /^\d{3}-\d{2}-\d{3}$/; if (!re.test(pin)) { throw new Error('Invalid PIN format, Make sure Format is XXX-XX-XXX'); } const invalidPins = [ '000-00-000', '111-11-111', '222-22-222', '333-33-333', '444-44-444', '555-55-555', '666-66-666', '777-77-777', '888-88-888', '999-99-999', '123-45-678', '876-54-321', ]; if (invalidPins.includes(pin)) { throw new Error('Invalid PIN'); } } /** * Build step 1 of the pair setup process. * * @param {PairMethods} [pairMethod] - Method to use for pairing, default is PairSetupWithAuth * @param {PairingTypeFlags} [pairFlags] - Flags to use for Pairing for PairSetup * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairSetupM1(pairMethod = PairMethods.PairSetupWithAuth, pairFlags = 0) { const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_Method, Buffer.from([pairMethod])); this.pairSetup.pairMethod = pairMethod; if (pairMethod === PairMethods.PairSetup && pairFlags) { data.set(exports.Types.kTLVType_Flags, Buffer.from([pairFlags])); this.pairSetup.pairTypeFlags = pairFlags; } const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the pair setup process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairSetupM2(m2Buffer) { const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } if (!tlv.has(exports.Types.kTLVType_PublicKey)) { throw new Error('M2: Missing public key'); } if (!tlv.has(exports.Types.kTLVType_Salt)) { throw new Error('M2: Missing salt'); } return tlv; } /** * Build step 3 of the pair setup process. * * @param {Object} m2Tlv - TLV object containing M2 response * @param {string} pin - Setup PIN * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairSetupM3(m2Tlv, pin) { const key = await fast_srp_hap_1.SRP.genKey(32); this.srpClient = new fast_srp_hap_1.SrpClient(fast_srp_hap_1.SRP.params.hap, m2Tlv.get(exports.Types.kTLVType_Salt), Buffer.from('Pair-Setup'), Buffer.from(pin), key); this.srpClient.setB(m2Tlv.get(exports.Types.kTLVType_PublicKey)); const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M3])); data.set(exports.Types.kTLVType_PublicKey, this.srpClient.computeA()); data.set(exports.Types.kTLVType_Proof, this.srpClient.computeM1()); return (0, tlv_1.encodeObject)(data); } /** * Parse step 4 of the pair setup process. * * @param {Buffer} m4Buffer - Buffer containing M4 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairSetupM4(m4Buffer) { if (!this.srpClient) { throw new Error('M4: SRP client not yet created'); } const tlv = (0, tlv_1.decodeBuffer)(m4Buffer); if (!tlv || tlv.size === 0) { throw new Error('M4: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M4: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M4: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M4) { throw new Error(`M4: Invalid state: ${state}`); } if (!tlv.has(exports.Types.kTLVType_Proof)) { throw new Error('M4: Proof missing from TLV'); } try { this.srpClient.checkM2(tlv.get(exports.Types.kTLVType_Proof)); } catch (e) { throw new Error(`M4: Proof verification failed: ${e}`); } return tlv; } /** * Build step 5 of the pair setup process. * * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairSetupM5() { if (!this.srpClient) { throw new Error('M5: SRP client not yet created'); } await libsodium_wrappers_1.default.ready; const seed = Buffer.from(libsodium_wrappers_1.default.randombytes_buf(32)); const key = libsodium_wrappers_1.default.crypto_sign_seed_keypair(seed); this.iOSDeviceLTSK = Buffer.from(key.privateKey); this.iOSDeviceLTPK = Buffer.from(key.publicKey); const hkdf1 = new node_hkdf_sync_1.default('sha512', 'Pair-Setup-Controller-Sign-Salt', this.srpClient.computeK()); const iOSDeviceX = hkdf1.derive('Pair-Setup-Controller-Sign-Info', 32); this.iOSDevicePairingID = Buffer.from((0, uuid_1.v4)()); const iOSDeviceInfo = Buffer.concat([iOSDeviceX, this.iOSDevicePairingID, this.iOSDeviceLTPK]); const iOSDeviceSignature = Buffer.from(libsodium_wrappers_1.default.crypto_sign_detached(iOSDeviceInfo, this.iOSDeviceLTSK)); const subData = new Map(); subData.set(exports.Types.kTLVType_Identifier, this.iOSDevicePairingID); subData.set(exports.Types.kTLVType_PublicKey, this.iOSDeviceLTPK); subData.set(exports.Types.kTLVType_Signature, iOSDeviceSignature); const subTlv = (0, tlv_1.encodeObject)(subData); const hkdf2 = new node_hkdf_sync_1.default('sha512', 'Pair-Setup-Encrypt-Salt', this.srpClient.computeK()); this.pairSetup.sessionKey = hkdf2.derive('Pair-Setup-Encrypt-Info', 32); const encryptedData = Buffer.from(libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_encrypt(subTlv, null, null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PS-Msg05')]), this.pairSetup.sessionKey)); const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M5])); data.set(exports.Types.kTLVType_EncryptedData, encryptedData); const tlv = (0, tlv_1.encodeObject)(data); return tlv; } /** * Parse step 6 of the pair setup process. * * @param {Buffer} m6Buffer - Buffer containing M4 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairSetupM6(m6Buffer) { if (!this.srpClient) { throw new Error('M5: SRP client not yet created'); } if (!this.pairSetup.sessionKey) { throw new Error('M6: Session key not yet set'); } await libsodium_wrappers_1.default.ready; const tlv = (0, tlv_1.decodeBuffer)(m6Buffer); if (!tlv || tlv.size === 0) { throw new Error('M6: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M6: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M6: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M6) { throw new Error(`M6: Invalid state: ${state}`); } if (!tlv.has(exports.Types.kTLVType_EncryptedData)) { throw new Error('M6: Encrypted data missing from TLV'); } let decryptedData; try { decryptedData = Buffer.from(libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_decrypt(null, tlv.get(exports.Types.kTLVType_EncryptedData), null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PS-Msg06')]), this.pairSetup.sessionKey)); } catch (_e) { throw new Error('M6: Decryption of sub-TLV failed'); } const subTlv = (0, tlv_1.decodeBuffer)(decryptedData); if (!subTlv.has(exports.Types.kTLVType_Signature)) { throw new Error('M6: Signature missing from sub-TLV'); } if (!subTlv.has(exports.Types.kTLVType_Identifier)) { throw new Error('M6: Identifier missing from sub-TLV'); } if (!subTlv.has(exports.Types.kTLVType_PublicKey)) { throw new Error('M6: Public key missing from sub-TLV'); } const hkdf = new node_hkdf_sync_1.default('sha512', 'Pair-Setup-Accessory-Sign-Salt', this.srpClient.computeK()); const AccessoryX = hkdf.derive('Pair-Setup-Accessory-Sign-Info', 32); this.AccessoryPairingID = subTlv.get(exports.Types.kTLVType_Identifier); this.AccessoryLTPK = subTlv.get(exports.Types.kTLVType_PublicKey); const AccessorySignature = subTlv.get(exports.Types.kTLVType_Signature); const AccessoryInfo = Buffer.concat([AccessoryX, this.AccessoryPairingID, this.AccessoryLTPK]); if (libsodium_wrappers_1.default.crypto_sign_verify_detached(AccessorySignature, AccessoryInfo, this.AccessoryLTPK)) { return subTlv; } else { throw new Error('M6: Signature verification failed'); } } /** * Build step 1 of the pair verify process. * * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairVerifyM1() { await libsodium_wrappers_1.default.ready; this.pairVerify.privateKey = Buffer.from(libsodium_wrappers_1.default.randombytes_buf(32)); this.pairVerify.publicKey = Buffer.from(libsodium_wrappers_1.default.crypto_scalarmult_base(this.pairVerify.privateKey)); const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_PublicKey, this.pairVerify.publicKey); const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the pair verify process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairVerifyM2(m2Buffer) { var _a; if (!this.AccessoryLTPK) { throw new Error('M2: Accessory LTPK not yet set'); } if (!this.pairVerify.privateKey) { throw new Error('M2: Private key not yet set'); } await libsodium_wrappers_1.default.ready; const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } if (!tlv.has(exports.Types.kTLVType_PublicKey)) { throw new Error('M2: Public key missing from TLV'); } if (!tlv.has(exports.Types.kTLVType_EncryptedData)) { throw new Error('M2: Encrypted data missing from TLV'); } this.pairVerify.accessoryPublicKey = tlv.get(exports.Types.kTLVType_PublicKey); this.pairVerify.sharedSecret = Buffer.from(libsodium_wrappers_1.default.crypto_scalarmult(this.pairVerify.privateKey, this.pairVerify.accessoryPublicKey)); const hkdf1 = new node_hkdf_sync_1.default('sha512', 'Pair-Verify-Encrypt-Salt', this.pairVerify.sharedSecret); this.pairVerify.sessionKey = hkdf1.derive('Pair-Verify-Encrypt-Info', 32); const hkdf2 = new node_hkdf_sync_1.default('sha512', 'Pair-Verify-Resume-Salt', this.pairVerify.sharedSecret); this.pairVerify.sessionID = hkdf2.derive('Pair-Verify-Resume-Info', 8); let decryptedData; try { decryptedData = Buffer.from(libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_decrypt(null, tlv.get(exports.Types.kTLVType_EncryptedData), null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PV-Msg02')]), this.pairVerify.sessionKey)); } catch (_e) { throw new Error('M2: Decryption of sub-TLV failed'); } const subTlv = (0, tlv_1.decodeBuffer)(decryptedData); if (!subTlv.has(exports.Types.kTLVType_Signature)) { throw new Error('M2: Signature missing from sub-TLV'); } if (!subTlv.has(exports.Types.kTLVType_Identifier)) { throw new Error('M2: Identifier missing from sub-TLV'); } const AccessoryPairingID = subTlv.get(exports.Types.kTLVType_Identifier).toString(); if (AccessoryPairingID !== ((_a = this.AccessoryPairingID) === null || _a === void 0 ? void 0 : _a.toString())) { throw new Error('M2: Wrong accessory pairing ID'); } const AccessoryInfo = Buffer.concat([ this.pairVerify.accessoryPublicKey, this.AccessoryPairingID, Buffer.from(libsodium_wrappers_1.default.crypto_scalarmult_base(this.pairVerify.privateKey)), ]); const signature = subTlv.get(exports.Types.kTLVType_Signature); if (libsodium_wrappers_1.default.crypto_sign_verify_detached(signature, AccessoryInfo, this.AccessoryLTPK)) { return subTlv; } else { throw new Error('M2: Signature verification failed'); } } /** * Build step 3 of the pair verify process. * * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairVerifyM3() { await libsodium_wrappers_1.default.ready; if (!this.pairVerify.publicKey) { throw new Error('M3: Public key not yet set'); } if (!this.pairVerify.accessoryPublicKey) { throw new Error('M3: Accessory public key not yet set'); } if (!this.pairVerify.sessionID) { throw new Error('M3: Session ID not yet set'); } if (!this.iOSDevicePairingID) { throw new Error('M3: iOS device pairing ID not yet set'); } if (!this.iOSDeviceLTSK) { throw new Error('M3: iOS device LTSK not yet set'); } const iOSDeviceInfo = Buffer.concat([ this.pairVerify.publicKey, this.iOSDevicePairingID, this.pairVerify.accessoryPublicKey, ]); const iOSDeviceSignature = Buffer.from(libsodium_wrappers_1.default.crypto_sign_detached(iOSDeviceInfo, this.iOSDeviceLTSK)); const subData = new Map(); subData.set(exports.Types.kTLVType_Identifier, Buffer.from(this.iOSDevicePairingID)); subData.set(exports.Types.kTLVType_Signature, iOSDeviceSignature); const subTlv = (0, tlv_1.encodeObject)(subData); const encryptedData = Buffer.from(libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_encrypt(subTlv, null, null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PV-Msg03')]), this.pairVerify.sessionKey)); const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M3])); data.set(exports.Types.kTLVType_EncryptedData, encryptedData); const tlv = (0, tlv_1.encodeObject)(data); return tlv; } /** * Parse step 4 of the pair verify process. * * @param {Buffer} m4Buffer - Buffer containing M4 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairVerifyM4(m4Buffer) { const tlv = (0, tlv_1.decodeBuffer)(m4Buffer); if (!tlv || tlv.size === 0) { throw new Error('M4: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M4: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M4: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M4) { throw new Error(`M4: Invalid state: ${state}`); } return tlv; } /** * Get the session keys generated by the PairVerify process. * * @returns {Object} * { * AccessoryToControllerKey: {Buffer}, * ControllerToAccessoryKey: {Buffer}, * } */ getSessionKeys() { if (!this.pairVerify.sharedSecret) { throw new Error('Shared secret not yet set'); } const salt = new node_hkdf_sync_1.default('sha512', 'Control-Salt', this.pairVerify.sharedSecret); this.sessionKeys.controllerToAccessoryKey = salt.derive('Control-Write-Encryption-Key', 32); this.sessionKeys.accessoryToControllerKey = salt.derive('Control-Read-Encryption-Key', 32); return { AccessoryToControllerKey: this.sessionKeys.accessoryToControllerKey, ControllerToAccessoryKey: this.sessionKeys.controllerToAccessoryKey, }; } /** * Build step 1 of the add pairing process. * * @param {string} identifier - Identifier of the new controller * @param {Buffer} ltpk - Long-term public key of the new controller * @param {boolean} isAdmin - Whether or not the new controller is an admin * @returns {Promise} Promise which resolves to a Buffer. */ async buildAddPairingM1(identifier, ltpk, isAdmin) { const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_Method, Buffer.from([Methods.AddPairing])); data.set(exports.Types.kTLVType_Identifier, Buffer.from(identifier)); data.set(exports.Types.kTLVType_PublicKey, ltpk); data.set(exports.Types.kTLVType_Permissions, Buffer.from([isAdmin ? 1 : 0])); const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the add pairing process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parseAddPairingM2(m2Buffer) { const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } return tlv; } /** * Build step 1 of the remove pairing process. * * @param {Buffer} identifier - Identifier of the controller to remove * @returns {Promise} Promise which resolves to a Buffer. */ async buildRemovePairingM1(identifier) { const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_Method, Buffer.from([Methods.RemovePairing])); data.set(exports.Types.kTLVType_Identifier, identifier); const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the remove pairing process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parseRemovePairingM2(m2Buffer) { const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } return tlv; } /** * Build step 1 of the list pairings process. * * @returns {Promise} Promise which resolves to a Buffer. */ async buildListPairingsM1() { const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_Method, Buffer.from([Methods.ListPairings])); const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the list pairings process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parseListPairingsM2(m2Buffer) { const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } return tlv; } /** * Build step 1 of the pair resume process. * * @returns {Promise} Promise which resolves to a Buffer. */ async buildPairResumeM1() { if (!this.pairVerify.sharedSecret) { throw new Error('M1: Shared secret not yet set'); } await libsodium_wrappers_1.default.ready; if (!this.pairVerify.sessionID) { throw new Error('M1: Session ID not yet set'); } this.pairVerify.privateKey = Buffer.from(libsodium_wrappers_1.default.randombytes_buf(32)); this.pairVerify.publicKey = Buffer.from(libsodium_wrappers_1.default.crypto_scalarmult_base(this.pairVerify.privateKey)); const hkdf = new node_hkdf_sync_1.default('sha512', Buffer.concat([this.pairVerify.publicKey, this.pairVerify.sessionID]), this.pairVerify.sharedSecret); const requestKey = hkdf.derive('Pair-Resume-Request-Info', 32); const encryptedData = Buffer.from(libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_encrypt(Buffer.alloc(0), null, null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PR-Msg01')]), requestKey)); const data = new Map(); data.set(exports.Types.kTLVType_State, Buffer.from([Steps.M1])); data.set(exports.Types.kTLVType_Method, Buffer.from([Methods.PairResume])); data.set(exports.Types.kTLVType_PublicKey, this.pairVerify.publicKey); data.set(exports.Types.kTLVType_SessionID, this.pairVerify.sessionID); data.set(exports.Types.kTLVType_EncryptedData, encryptedData); const packet = (0, tlv_1.encodeObject)(data); return packet; } /** * Parse step 2 of the pair resume process. * * @param {Buffer} m2Buffer - Buffer containing M2 response * @returns {Promise} Promise which resolves to a TLV object. */ async parsePairResumeM2(m2Buffer) { await libsodium_wrappers_1.default.ready; if (!this.pairVerify.publicKey) { throw new Error('M2: Public key not yet set'); } if (!this.pairVerify.sharedSecret) { throw new Error('M2: Shared secret not yet set'); } const tlv = (0, tlv_1.decodeBuffer)(m2Buffer); if (!tlv || tlv.size === 0) { throw new Error('M2: Empty TLV'); } if (tlv.has(exports.Types.kTLVType_Error)) { const errorCode = tlv.get(exports.Types.kTLVType_Error).readUInt8(0); throw new error_1.default(`M2: Error: ${errorCode}`, errorCode); } if (!tlv.has(exports.Types.kTLVType_State)) { throw new Error('M2: Missing state'); } const state = tlv.get(exports.Types.kTLVType_State)[0]; if (state !== Steps.M2) { throw new Error(`M2: Invalid state: ${state}`); } if (!tlv.has(exports.Types.kTLVType_SessionID)) { throw new Error('M2: Session ID missing from TLV'); } if (!tlv.has(exports.Types.kTLVType_EncryptedData)) { throw new Error('M2: Encrypted data missing from TLV'); } this.pairVerify.sessionID = tlv.get(exports.Types.kTLVType_SessionID); const hkdf1 = new node_hkdf_sync_1.default('sha512', Buffer.concat([this.pairVerify.publicKey, this.pairVerify.sessionID]), this.pairVerify.sharedSecret); const responseKey = hkdf1.derive('Pair-Resume-Response-Info', 32); try { libsodium_wrappers_1.default.crypto_aead_chacha20poly1305_ietf_decrypt(null, tlv.get(exports.Types.kTLVType_EncryptedData), null, Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('PR-Msg02')]), responseKey); } catch (_e) { throw new Error('M2: Decryption of data failed'); } const hkdf2 = new node_hkdf_sync_1.default('sha512', Buffer.concat([this.pairVerify.publicKey, this.pairVerify.sessionID]), this.pairVerify.sharedSecret); this.pairVerify.sharedSecret = hkdf2.derive('Pair-Resume-Shared-Secret-Info', 32); return tlv; } /** * Get the data (keys) that needs to be stored long-term. * * @returns {PairingProtocol} Object containing the keys that should be stored. */ getLongTermData() { if (!this.AccessoryPairingID || !this.AccessoryLTPK || !this.iOSDevicePairingID || !this.iOSDeviceLTSK || !this.iOSDeviceLTPK) { return null; } return { AccessoryPairingID: PairingProtocol.bufferToHex(this.AccessoryPairingID), AccessoryLTPK: PairingProtocol.bufferToHex(this.AccessoryLTPK), iOSDevicePairingID: PairingProtocol.bufferToHex(this.iOSDevicePairingID), iOSDeviceLTSK: PairingProtocol.bufferToHex(this.iOSDeviceLTSK), iOSDeviceLTPK: PairingProtocol.bufferToHex(this.iOSDeviceLTPK), }; } } exports.default = PairingProtocol; //# sourceMappingURL=pairing-protocol.js.map