UNPKG

@domojs/homekit-controller

Version:

217 lines 9.51 kB
import { SRP, SrpClient } from "fast-srp-hap"; import tweetnacl from "tweetnacl"; import { IsomorphicBuffer } from "@akala/core"; import hkdf from 'futoin-hkdf'; import { Cursor, parsers, parserWrite, tlv } from '@akala/protocol-parser'; import assert from 'assert/strict'; const tlv8 = tlv(parsers.uint8, 0xFF, 'utf8'); export var PairMethod; (function (PairMethod) { PairMethod[PairMethod["Setup"] = 0] = "Setup"; PairMethod[PairMethod["SetupWithAuth"] = 1] = "SetupWithAuth"; PairMethod[PairMethod["Verify"] = 2] = "Verify"; PairMethod[PairMethod["AddPairing"] = 3] = "AddPairing"; PairMethod[PairMethod["RemovePairing"] = 4] = "RemovePairing"; PairMethod[PairMethod["ListPairings"] = 5] = "ListPairings"; })(PairMethod || (PairMethod = {})); export var PairState; (function (PairState) { PairState[PairState["M1"] = 1] = "M1"; PairState[PairState["M2"] = 2] = "M2"; PairState[PairState["M3"] = 3] = "M3"; PairState[PairState["M4"] = 4] = "M4"; PairState[PairState["M5"] = 5] = "M5"; PairState[PairState["M6"] = 6] = "M6"; })(PairState || (PairState = {})); export var PairErrorCode; (function (PairErrorCode) { PairErrorCode[PairErrorCode["Unknown"] = 1] = "Unknown"; PairErrorCode[PairErrorCode["Authentication"] = 2] = "Authentication"; PairErrorCode[PairErrorCode["Backoff"] = 3] = "Backoff"; PairErrorCode[PairErrorCode["MaxPeers"] = 4] = "MaxPeers"; PairErrorCode[PairErrorCode["MaxTries"] = 5] = "MaxTries"; PairErrorCode[PairErrorCode["Unavailable"] = 6] = "Unavailable"; PairErrorCode[PairErrorCode["Busy"] = 7] = "Busy"; })(PairErrorCode || (PairErrorCode = {})); export var PairTypeFlags; (function (PairTypeFlags) { PairTypeFlags[PairTypeFlags["Transient"] = 2] = "Transient"; PairTypeFlags[PairTypeFlags["Split"] = 64] = "Split"; })(PairTypeFlags || (PairTypeFlags = {})); export default async function pair(accessoryAddress, accessoryFqdn, http, pinCode) { const clientKeyPair = tweetnacl.sign.keyPair(); const clientInfo = { privateKey: new IsomorphicBuffer(clientKeyPair.secretKey), publicKey: new IsomorphicBuffer(clientKeyPair.publicKey), username: crypto.getRandomValues(Buffer.alloc(16)).toString('base64') }; const accessory = await new PairSetupClient(accessoryAddress, http).sendPairSetup(pinCode, clientInfo); this.pairedAccessories[accessoryFqdn] = { controllerInfo: clientInfo, accessory, fqdn: accessoryFqdn }; } export const pairMessage = tlv8.objectByName({ method: { index: 0, parser: tlv8.number }, identifier: { index: 1, parser: tlv8.string }, salt: { index: 2, parser: tlv8.buffer }, publicKey: { index: 3, parser: tlv8.buffer }, proof: { index: 4, parser: tlv8.buffer }, encryptedData: { index: 5, parser: tlv8.buffer }, state: { index: 6, parser: tlv8.number }, error: { index: 7, parser: tlv8.number }, retryDelay: { index: 8, parser: tlv8.number }, certificate: { index: 9, parser: tlv8.number }, signature: { index: 10, parser: tlv8.number }, permissions: { index: 11, parser: tlv8.number }, fragmentData: { index: 12, parser: tlv8.number }, fragmentLast: { index: 13, parser: tlv8.number }, flags: { index: 0x13, parser: tlv8.number }, }); /** * @group Cryptography */ export function chacha20_poly1305_encryptAndSeal(key, nonce, aad, plaintext) { if (nonce.length < 12) { // openssl 3.x.x requires 98 bits nonce length nonce = Buffer.concat([ Buffer.alloc(12 - nonce.length, 0), nonce, ]); } // @ts-expect-error: types for this are really broken const cipher = crypto.createCipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 }); if (aad) { cipher.setAAD(aad); } const ciphertext = cipher.update(plaintext); cipher.final(); // final call creates the auth tag const authTag = cipher.getAuthTag(); return { ciphertext: ciphertext, authTag: authTag, }; } /** * @group Cryptography */ export function chacha20_poly1305_decryptAndVerify(key, nonce, aad, ciphertext, authTag) { if (nonce.length < 12) { // openssl 3.x.x requires 98 bits nonce length nonce = Buffer.concat([ Buffer.alloc(12 - nonce.length, 0), nonce, ]); } // @ts-expect-error: types for this are really broken const decipher = crypto.createDecipheriv("chacha20-poly1305", key, nonce, { authTagLength: 16 }); if (aad) { decipher.setAAD(aad); } decipher.setAuthTag(authTag); const plaintext = decipher.update(ciphertext); decipher.final(); // final call verifies integrity using the auth tag. Throws error if something was manipulated! return plaintext; } class PairSetupClient { accessoryAddress; http; constructor(accessoryAddress, http) { this.accessoryAddress = accessoryAddress; this.http = http; } async sendPairSetup(pincode, clientInfo) { const m2 = await this.sendM1(); const srp = await this.prepareM3(m2, pincode); const m4 = await this.sendM3(srp); const m5 = this.prepareM5(m4, clientInfo); return await this.sendM5(m5.encryptedData, m5.sessionKey); } sendM1() { return this.http.call({ url: `http://${this.accessoryAddress}/pair-setup`, body: parserWrite(pairMessage, { state: PairState.M1, method: PairMethod.Setup, flags: PairTypeFlags.Split & PairTypeFlags.Transient }).toArray().buffer, method: 'post', type: 'raw' }). then(r => r.arrayBuffer()). then(b => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(b), new Cursor())). then(m => { assert.equal(m.state, PairState.M2, 'an M2 response was expected'); return m; }); } async prepareM3(m2, pincode) { const srpKey = await SRP.genKey(32); const srpClient = new SrpClient(SRP.params.hap, Buffer.from(m2.salt.toArray()), Buffer.from("Pair-Setup"), Buffer.from(pincode), srpKey); srpClient.setB(Buffer.from(m2.publicKey.toArray())); return srpClient; } sendM3(m3) { return this.http.call({ url: `http://${this.accessoryAddress}/pair-setup`, body: parserWrite(pairMessage, { state: PairState.M3, publicKey: IsomorphicBuffer.fromBuffer(m3.computeA()), proof: IsomorphicBuffer.fromBuffer(m3.computeM1()) }).toArray().buffer, type: 'raw' }). then(r => r.arrayBuffer()). then(b => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(b), new Cursor())). then(b => { m3.checkM2(Buffer.from(b.proof.toArray())); return b; }). then(b => m3.computeK()) //sharedSecret ; } prepareM5(sharedSecret, clientInfo) { const iOSDeviceX = hkdf(sharedSecret, 32, { hash: 'sha512', salt: Buffer.from("Pair-Setup-Controller-Sign-Salt"), info: Buffer.from("Pair-Setup-Controller-Sign-Info"), }); const iOSDeviceInfo = IsomorphicBuffer.concat([ IsomorphicBuffer.fromBuffer(iOSDeviceX), IsomorphicBuffer.from(clientInfo.username), clientInfo.publicKey, ]); const iOSDeviceSignature = tweetnacl.sign.detached(iOSDeviceInfo.toArray(), clientInfo.privateKey.toArray()); const subTLV_M5 = parserWrite(pairMessage, { identifier: clientInfo.username, publicKey: clientInfo.publicKey, signature: new IsomorphicBuffer(iOSDeviceSignature) }); const sessionKey = hkdf(sharedSecret, 32, { hash: "sha512", salt: Buffer.from("Pair-Setup-Encrypt-Salt"), info: Buffer.from("Pair-Setup-Encrypt-Info"), }); const encrypted = chacha20_poly1305_encryptAndSeal(sessionKey, Buffer.from("PS-Msg05"), null, Buffer.from(subTLV_M5.toArray())); return { sessionKey, encryptedData: encrypted, }; } sendM5(m5, sessionKey) { return this.http.call({ url: `http://${this.accessoryAddress}/pair-setup`, body: parserWrite(pairMessage, { state: PairState.M6, encryptedData: IsomorphicBuffer.concat([m5.ciphertext, m5.authTag]) }).toArray().buffer, type: 'raw' }). then(r => r.arrayBuffer()). then(r => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(r), new Cursor())). then(m => m.encryptedData). then(m => ({ encryptedData: m.subarray(0, -16), authTag: m.subarray(-16) })). then(m => chacha20_poly1305_decryptAndVerify(sessionKey, Buffer.from("PS-Msg06"), null, Buffer.from(m.encryptedData.toArray()), Buffer.from(m.authTag.toArray()))). then(m => pairMessage.read(new IsomorphicBuffer(m), new Cursor())). then(m => { if (tweetnacl.sign.detached.verify(Buffer.concat([sessionKey, Buffer.from(m.identifier), m.publicKey.toArray()]), m.signature.toArray(), m.publicKey.toArray())) return { publicKey: m.publicKey, identifier: m.identifier }; throw new Error('accessory is not what it is claiming to be'); }); } } //# sourceMappingURL=setup-pair.js.map