@domojs/homekit-controller
Version:
145 lines • 6.79 kB
JavaScript
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: parserWrite(pairMessage, {
state: PairState.M1,
publicKey: new IsomorphicBuffer(this.keyPair.publicKey),
}).toArray().buffer,
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 = 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: parserWrite(pairMessage, {
state: PairState.M3,
encryptedData: IsomorphicBuffer.concat([encrypted_M3.ciphertext, encrypted_M3.authTag]),
}).toArray().buffer,
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