UNPKG

@domojs/homekit-controller

Version:

145 lines 6.86 kB
import { IsomorphicBuffer } from "@akala/core"; import { Cursor, parserWrite } from "@akala/protocol-parser"; import { chacha20_poly1305_decryptAndVerify, chacha20_poly1305_encryptAndSeal, pairMessage, PairState } from "./setup-pair.js"; import assert from 'assert/strict'; import hkdf from 'futoin-hkdf'; import tweetnacl from "tweetnacl"; export default async function verifyPair(accessoryFqdn) { if (!this.pairedAccessories[accessoryFqdn]) throw new Error('There is no such accessory'); } export class HAPEncryption { clientPublicKey; secretKey; publicKey; sharedSecret; hkdfPairEncryptionKey; accessoryToControllerCount = 0; controllerToAccessoryCount = 0; accessoryToControllerKey; controllerToAccessoryKey; incompleteFrame; constructor(clientPublicKey, secretKey, publicKey, sharedSecret, hkdfPairEncryptionKey) { this.clientPublicKey = clientPublicKey; this.secretKey = secretKey; this.publicKey = publicKey; this.sharedSecret = sharedSecret; this.hkdfPairEncryptionKey = hkdfPairEncryptionKey; this.accessoryToControllerKey = Buffer.alloc(0); this.controllerToAccessoryKey = Buffer.alloc(0); } } class PairSetupClient { accessoryAddress; http; accessories; keyPair; constructor(accessoryAddress, http, accessories) { this.accessoryAddress = accessoryAddress; this.http = http; this.accessories = accessories; this.keyPair = tweetnacl.box.keyPair(); } async sendPairVerify() { // M1 const m2 = await this.sendM1(); const m4 = await this.sendM3(m2); // verify that encryption works! const encryption = new HAPEncryption(Buffer.from(m2.pairedAccessory.accessory.publicKey.toArray()), Buffer.from(m2.pairedAccessory.controllerInfo.privateKey.toArray()), Buffer.from(m2.pairedAccessory.controllerInfo.publicKey.toArray()), m2.sharedSecret, m2.sessionKey); // our HAPCrypto is engineered for the server side, so we have to switch the keys here (deliberately wrongfully) // such that hapCrypto uses the controllerToAccessoryKey for encryption! encryption.accessoryToControllerKey = m4.controllerToAccessoryKey; encryption.controllerToAccessoryKey = m4.accessoryToControllerKey; return encryption; } async sendM1() { return await this.http.call({ url: `http://${this.accessoryAddress}/pair-verify`, body: IsomorphicBuffer.concat(parserWrite(pairMessage, { state: PairState.M1, publicKey: new IsomorphicBuffer(this.keyPair.publicKey), })).toArray(), method: 'post', type: 'raw' }). then(async (r) => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(await r.arrayBuffer()), new Cursor())). then(m => { assert.equal(m.state, PairState.M2, 'an M2 response was expected'); const accessoryPublicKey = m.publicKey; const sharedSecret = Buffer.from(tweetnacl.scalarMult(this.keyPair.secretKey, accessoryPublicKey.toArray())); const sessionKey = hkdf(sharedSecret, 32, { hash: "sha512", salt: Buffer.from("Pair-Verify-Encrypt-Salt"), info: Buffer.from("Pair-Verify-Encrypt-Info"), }); const cipherTextM2 = m.encryptedData.subarray(0, -16); const authTagM2 = m.encryptedData.subarray(-16); const plaintextM2 = new IsomorphicBuffer(0); chacha20_poly1305_decryptAndVerify(sessionKey, Buffer.from("PV-Msg02"), null, Buffer.from(cipherTextM2.toArray()), Buffer.from(authTagM2.toArray())); const m2 = pairMessage.read(plaintextM2, new Cursor()); const accessoryIdentifier = m2.identifier; const pairedAccessory = Object.values(this.accessories).find(a => a.accessory.identifier == accessoryIdentifier); if (!pairedAccessory) throw new Error('The accessory is not recorded as paired, please run setup-pair beforehand'); const accessorySignature = m2.signature; const accessoryInfo = Buffer.concat([ accessoryPublicKey.toArray(), Buffer.from(m2.identifier), this.keyPair.publicKey, ]); if (!tweetnacl.sign.detached.verify(accessoryInfo, accessorySignature.toArray(), pairedAccessory.controllerInfo.publicKey.toArray())) throw new Error('signature could not be verified'); return { sharedSecret, sessionKey, serverEphemeralPublicKey: accessoryPublicKey, pairedAccessory }; }); } async sendM3(m2) { const controllerInfo = Buffer.concat([ this.keyPair.publicKey, Buffer.from(m2.pairedAccessory.controllerInfo.username), m2.serverEphemeralPublicKey.toArray(), ]); // step 8 const controllerSignature = new IsomorphicBuffer(tweetnacl.sign.detached(controllerInfo, m2.pairedAccessory.controllerInfo.privateKey.toArray())); // step 9 const plainTextTLV_M3 = IsomorphicBuffer.concat(parserWrite(pairMessage, { identifier: m2.pairedAccessory.accessory.identifier, signature: controllerSignature, })); // step 10 const encrypted_M3 = chacha20_poly1305_encryptAndSeal(m2.sessionKey, Buffer.from("PV-Msg03"), null, Buffer.from(plainTextTLV_M3.toArray())); return this.http.call({ url: `http://${this.accessoryAddress}/pair-verify`, body: IsomorphicBuffer.concat(parserWrite(pairMessage, { state: PairState.M3, encryptedData: IsomorphicBuffer.concat([encrypted_M3.ciphertext, encrypted_M3.authTag]), })).toArray(), type: "raw" }). then(r => r.arrayBuffer()). then(b => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(b), new Cursor())). then(m4 => { assert.equal(m4.state, PairState.M4); assert.equal(m4.error, undefined); const salt = Buffer.from("Control-Salt"); const accessoryToControllerKey = hkdf(m2.sharedSecret, 32, { hash: "sha512", salt, info: Buffer.from("Control-Read-Encryption-Key"), }); const controllerToAccessoryKey = hkdf(m2.sharedSecret, 32, { hash: "sha512", salt, info: Buffer.from("Control-Write-Encryption-Key"), }); return { accessoryToControllerKey, controllerToAccessoryKey, }; }); } } //# sourceMappingURL=verify-pair.js.map