@domojs/homekit-controller
Version:
217 lines • 9.51 kB
JavaScript
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