hap-controller
Version:
Library to implement a HAP (HomeKit) controller
777 lines • 33 kB
JavaScript
"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