@domojs/homekit-controller
Version:
242 lines (202 loc) • 8.78 kB
text/typescript
import { Http, IsomorphicBuffer, parser } from "@akala/core";
import { Cursor, parserWrite } from "@akala/protocol-parser";
import State from "../state.js";
import { chacha20_poly1305_decryptAndVerify, chacha20_poly1305_encryptAndSeal, PairMessage, pairMessage, PairMethod, PairState, PairTypeFlags } from "./setup-pair.js";
import assert from 'assert/strict'
import { SrpClient } from "fast-srp-hap";
import hkdf from 'futoin-hkdf'
import tweetnacl from "tweetnacl";
export default async function verifyPair(this: State, accessoryFqdn: string)
{
if (!this.pairedAccessories[accessoryFqdn])
throw new Error('There is no such accessory');
}
type Unpromisify<T> = T extends Promise<infer X> ? X : never;
export type PairVerifyM2 = Pick<PairMessage, 'state' | 'publicKey' | 'encryptedData'>;
export type PairVerifyM3 = Pick<PairMessage, 'state' | 'proof'>;
export type PairVerifyM4 = Pick<PairMessage, 'state' | 'error'>;
export type PairVerifyM5 = Pick<PairMessage, 'state' | 'publicKey' | 'encryptedData'>;
export type PairVerifyM6 = Pick<PairMessage, 'state' | 'encryptedData'>;
export type SubPairVerifyM6 = Pick<PairMessage, 'identifier' | 'signature' | 'publicKey'>;
interface PairVerifyServerInfo
{
username: string;
publicKey: Buffer;
}
interface PairVerifyClientInfo
{
username: string;
privateKey: Buffer;
}
export class HAPEncryption
{
readonly clientPublicKey: Buffer;
readonly secretKey: Buffer;
readonly publicKey: Buffer;
readonly sharedSecret: Buffer;
readonly hkdfPairEncryptionKey: Buffer;
accessoryToControllerCount = 0;
controllerToAccessoryCount = 0;
accessoryToControllerKey: Buffer;
controllerToAccessoryKey: Buffer;
incompleteFrame?: Buffer;
public constructor(clientPublicKey: Buffer, secretKey: Buffer, publicKey: Buffer, sharedSecret: Buffer, hkdfPairEncryptionKey: Buffer)
{
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
{
private readonly keyPair: tweetnacl.BoxKeyPair;
constructor(private readonly accessoryAddress: string, private readonly http: Http, private readonly accessories: State['pairedAccessories'])
{
this.keyPair = tweetnacl.box.keyPair();
}
async sendPairVerify(): Promise<HAPEncryption>
{
// 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 as ArrayBuffer,
method: 'post',
type: 'raw'
}).
then(async r => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(await r.arrayBuffer()), new Cursor()) as PairVerifyM2).
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: Unpromisify<ReturnType<PairSetupClient['sendM1']>>)
{
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 as ArrayBuffer,
type: "raw"
}).
then(r => r.arrayBuffer()).
then(b => pairMessage.read(IsomorphicBuffer.fromArrayBuffer(b), new Cursor()) as PairVerifyM4).
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,
};
})
;
}
}