Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,7 @@
"fast-srp-hap": "^2.0.4",
"futoin-hkdf": "^1.5.3",
"source-map-support": "^0.5.21",
"tslib": "^2.8.1",
"tweetnacl": "^1.0.3"
"tslib": "^2.8.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.6",
Expand All @@ -82,6 +81,7 @@
"simple-plist": "^1.4.0",
"ts-jest": "^29.4.11",
"ts-node": "^10.9.2",
"tweetnacl": "^1.0.3",
"typedoc": "^0.28.20",
"typescript": "^6.0.3"
},
Expand Down
9 changes: 4 additions & 5 deletions src/lib/HAPServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import createDebug from "debug";
import { EventEmitter } from "events";
import { SRP, SrpServer } from "fast-srp-hap";
import { IncomingMessage, ServerResponse } from "http";
import tweetnacl from "tweetnacl";
import { URL } from "url";
import { consideredTrue, HAPMimeTypes, PairingStates, PairMethods, TLVValues } from "../internal-types";
import {
Expand Down Expand Up @@ -634,7 +633,7 @@ export class HAPServer extends EventEmitter {
const controllerInfo = Buffer.from("Pair-Setup-Controller-Sign-Info");
const outputKey = hapCrypto.HKDF("sha512", controllerSalt, S_private, controllerInfo, 32);
const completeData = Buffer.concat([outputKey, clientUsername, clientLTPK]);
if (!tweetnacl.sign.detached.verify(completeData, clientProof, clientLTPK)) {
if (!hapCrypto.ed25519Verify(completeData, clientProof, clientLTPK)) {
debug("[%s] Invalid signature", this.accessoryInfo.username);
response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" });
response.end(tlv.encode(TLVValues.SEQUENCE_NUM, PairingStates.M6, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION));
Expand Down Expand Up @@ -662,7 +661,7 @@ export class HAPServer extends EventEmitter {
const usernameData = Buffer.from(this.accessoryInfo.username);
const material = Buffer.concat([outputKey, usernameData, serverLTPK]);
const privateKey = Buffer.from(this.accessoryInfo.signSk);
const serverProof = tweetnacl.sign.detached(material, privateKey);
const serverProof = hapCrypto.ed25519Sign(material, privateKey);
const message = tlv.encode(TLVValues.USERNAME, usernameData, TLVValues.PUBLIC_KEY, serverLTPK, TLVValues.PROOF, serverProof);

const encrypted = hapCrypto.chacha20_poly1305_encryptAndSeal(hkdfEncKey, Buffer.from("PS-Msg06"), null, message);
Expand Down Expand Up @@ -723,7 +722,7 @@ export class HAPServer extends EventEmitter {
const usernameData = Buffer.from(this.accessoryInfo.username);
const material = Buffer.concat([publicKey, usernameData, clientPublicKey]);
const privateKey = Buffer.from(this.accessoryInfo.signSk);
const serverProof = tweetnacl.sign.detached(material, privateKey);
const serverProof = hapCrypto.ed25519Sign(material, privateKey);
const encSalt = Buffer.from("Pair-Verify-Encrypt-Salt");
const encInfo = Buffer.from("Pair-Verify-Encrypt-Info");
const outputKey = hapCrypto.HKDF("sha512", encSalt, sharedSec, encInfo, 32).subarray(0, 32);
Expand Down Expand Up @@ -804,7 +803,7 @@ export class HAPServer extends EventEmitter {
connection._pairVerifyState = undefined;
return;
}
if (!tweetnacl.sign.detached.verify(material, proof, clientPublicKey)) {
if (!hapCrypto.ed25519Verify(material, proof, clientPublicKey)) {
debug("[%s] Client %s provided an invalid signature", this.accessoryInfo.username, clientUsername);
response.writeHead(HAPPairingHTTPCode.OK, { "Content-Type": "application/pairing+tlv8" });
response.end(tlv.encode(TLVValues.STATE, PairingStates.M4, TLVValues.ERROR_CODE, TLVErrorCode.AUTHENTICATION));
Expand Down
4 changes: 2 additions & 2 deletions src/lib/model/AccessoryInfo.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import assert from "assert";
import crypto from "crypto";
import createDebug from "debug";
import tweetnacl from "tweetnacl";
import util from "util";
import { AccessoryJsonObject, CharacteristicJsonObject, MacAddress, ServiceJsonObject } from "../../types";
import { Categories } from "../Accessory";
import { EventedHTTPServer, HAPConnection, HAPUsername } from "../util/eventedhttp";
import { generateEd25519KeyPair } from "../util/hapCrypto";
import { HAPStorage } from "./HAPStorage";
import { readFileSync } from "node:fs";

Expand Down Expand Up @@ -301,7 +301,7 @@ export class AccessoryInfo {
accessoryInfo.lastFirmwareVersion = getVersion();

// Create a new unique key pair for this accessory.
const keyPair = tweetnacl.sign.keyPair();
const keyPair = generateEd25519KeyPair();

accessoryInfo.signSk = Buffer.from(keyPair.secretKey);
accessoryInfo.signPk = Buffer.from(keyPair.publicKey);
Expand Down
123 changes: 123 additions & 0 deletions src/lib/util/hapCrypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HAPEncryption } from "./eventedhttp";
import * as hapCrypto from "./hapCrypto";
import crypto from "crypto";
import tweetnacl from "tweetnacl";

function fromHex(hex: string): Buffer {
return Buffer.from(hex.trim().replace(/ /g, ""), "hex");
Expand Down Expand Up @@ -163,4 +164,126 @@ describe("hapCrypto", () => {
expect(S_decrypted1).toEqual(Buffer.concat([message1, message2]));
});
});

describe("curve25519", () => {
// RFC 7748, section 6.1
const alicePrivate = fromHex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a");
const alicePublic = fromHex("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a");
const bobPrivate = fromHex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb");
const bobPublic = fromHex("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f");
const shared = fromHex("4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742");

it("should compute the shared secret of the RFC 7748 test vector", () => {
expect(Buffer.from(hapCrypto.generateCurve25519SharedSecKey(alicePrivate, bobPublic))).toEqual(shared);
expect(Buffer.from(hapCrypto.generateCurve25519SharedSecKey(bobPrivate, alicePublic))).toEqual(shared);
});

it("should generate key pairs whose public key belongs to the secret key", () => {
for (let i = 0; i < 10; i++) {
const keyPair = hapCrypto.generateCurve25519KeyPair();
expect(keyPair.secretKey.length).toBe(32);
expect(Buffer.from(keyPair.publicKey)).toEqual(Buffer.from(tweetnacl.scalarMult.base(keyPair.secretKey)));
}
});

it("should compute the same shared secret as tweetnacl", () => {
for (let i = 0; i < 10; i++) {
const ours = hapCrypto.generateCurve25519KeyPair();
const theirs = tweetnacl.box.keyPair();
expect(Buffer.from(hapCrypto.generateCurve25519SharedSecKey(ours.secretKey, theirs.publicKey)))
.toEqual(Buffer.from(tweetnacl.scalarMult(theirs.secretKey, ours.publicKey)));
}
});

it("should return zeros for a public key of small order, like tweetnacl", () => {
const secretKey = hapCrypto.generateCurve25519KeyPair().secretKey;
// points of small order (0, 1, a point of order 8, p - 1)
const smallOrder = [
"0000000000000000000000000000000000000000000000000000000000000000",
"0100000000000000000000000000000000000000000000000000000000000000",
"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800",
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
].map(fromHex);
for (const publicKey of smallOrder) {
expect(Buffer.from(hapCrypto.generateCurve25519SharedSecKey(secretKey, publicKey))).toEqual(Buffer.alloc(32));
expect(Buffer.from(tweetnacl.scalarMult(secretKey, publicKey))).toEqual(Buffer.alloc(32));
}
});

it("should reject keys of the wrong size", () => {
expect(() => hapCrypto.generateCurve25519SharedSecKey(Buffer.alloc(31), alicePublic)).toThrow();
expect(() => hapCrypto.generateCurve25519SharedSecKey(alicePrivate, Buffer.alloc(33))).toThrow();
});
});

describe("ed25519", () => {
// RFC 8032, section 7.1, TEST 1 and TEST 2
const vectors = [
{
seed: fromHex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"),
publicKey: fromHex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"),
message: Buffer.alloc(0),
signature: fromHex(
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b",
),
},
{
seed: fromHex("4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"),
publicKey: fromHex("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c"),
message: fromHex("72"),
signature: fromHex(
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00",
),
},
];

it("should sign and verify the RFC 8032 test vectors", () => {
for (const vector of vectors) {
const secretKey = Buffer.concat([vector.seed, vector.publicKey]);
expect(hapCrypto.ed25519Sign(vector.message, secretKey)).toEqual(vector.signature);
expect(hapCrypto.ed25519Verify(vector.message, vector.signature, vector.publicKey)).toBe(true);
}
});

it("should generate key pairs in the layout of tweetnacl", () => {
const keyPair = hapCrypto.generateEd25519KeyPair();
expect(keyPair.secretKey.length).toBe(64);
const fromSeed = tweetnacl.sign.keyPair.fromSeed(keyPair.secretKey.subarray(0, 32));
expect(Buffer.from(keyPair.publicKey)).toEqual(Buffer.from(fromSeed.publicKey));
expect(Buffer.from(keyPair.secretKey)).toEqual(Buffer.from(fromSeed.secretKey));
});

it("should sign like tweetnacl and verify the signatures of tweetnacl", () => {
for (let i = 0; i < 10; i++) {
const keyPair = tweetnacl.sign.keyPair();
const message = crypto.randomBytes(i * 17);
const signature = tweetnacl.sign.detached(message, keyPair.secretKey);
expect(hapCrypto.ed25519Sign(message, keyPair.secretKey)).toEqual(Buffer.from(signature));
expect(hapCrypto.ed25519Verify(message, signature, keyPair.publicKey)).toBe(true);
}
});

it("should not verify a changed message, another key or a public key that is no point on the curve", () => {
const keyPair = hapCrypto.generateEd25519KeyPair();
const message = crypto.randomBytes(64);
const signature = hapCrypto.ed25519Sign(message, keyPair.secretKey);
const changed = Buffer.from(message);
changed[0] ^= 1;
expect(hapCrypto.ed25519Verify(changed, signature, keyPair.publicKey)).toBe(false);
expect(hapCrypto.ed25519Verify(message, signature, hapCrypto.generateEd25519KeyPair().publicKey)).toBe(false);
for (let i = 0; i < 10; i++) {
const publicKey = crypto.randomBytes(32);
expect(hapCrypto.ed25519Verify(message, signature, publicKey)).toBe(tweetnacl.sign.detached.verify(message, signature, publicKey));
}
});

it("should not verify a signature or public key of the wrong size", () => {
const keyPair = hapCrypto.generateEd25519KeyPair();
const message = crypto.randomBytes(16);
const signature = hapCrypto.ed25519Sign(message, keyPair.secretKey);
expect(hapCrypto.ed25519Verify(message, signature.subarray(0, 63), keyPair.publicKey)).toBe(false);
expect(hapCrypto.ed25519Verify(message, signature, keyPair.publicKey.subarray(0, 31))).toBe(false);
expect(() => hapCrypto.ed25519Sign(message, keyPair.secretKey.subarray(0, 32))).toThrow();
});
});
});
94 changes: 90 additions & 4 deletions src/lib/util/hapCrypto.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,112 @@
import assert from "assert";
import crypto from "crypto";
import hkdf from "futoin-hkdf";
import tweetnacl, { BoxKeyPair } from "tweetnacl";
import { HAPEncryption } from "./eventedhttp";

if (!crypto.getCiphers().includes("chacha20-poly1305")) {
assert.fail("The cipher 'chacha20-poly1305' is not supported with your current running nodejs version v" + process.version + ". " +
"At least a nodejs version of v10.17.0 (excluding v11.0 and v11.1) is required!");
}

const X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
const X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
const ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");

function privateKeyObject(prefix: Buffer, key: Uint8Array): crypto.KeyObject {
return crypto.createPrivateKey({ key: Buffer.concat([prefix, key]), format: "der", type: "pkcs8" });
}

function publicKeyObject(prefix: Buffer, key: Uint8Array): crypto.KeyObject {
return crypto.createPublicKey({ key: Buffer.concat([prefix, key]), format: "der", type: "spki" });
}

function rawKey(key: crypto.KeyObject): Buffer {
return key.export({ format: "der", type: key.type === "private" ? "pkcs8" : "spki" }).subarray(-32);
}

/**
* @group Cryptography
*/
export interface Curve25519KeyPair {
publicKey: Uint8Array;
secretKey: Uint8Array;
}

/**
* An Ed25519 key pair. The secret key has 64 bytes: the 32-byte seed followed by the public key
* (the layout of tweetnacl, in which the long-term keys of accessories are stored).
*
* @group Cryptography
*/
export function generateCurve25519KeyPair(): BoxKeyPair {
return tweetnacl.box.keyPair();
export interface Ed25519KeyPair {
publicKey: Uint8Array;
secretKey: Uint8Array;
}

/**
* @group Cryptography
*/
export function generateCurve25519KeyPair(): Curve25519KeyPair {
const { publicKey, privateKey } = crypto.generateKeyPairSync("x25519");
return { publicKey: rawKey(publicKey), secretKey: rawKey(privateKey) };
}

/**
* @group Cryptography
*/
export function generateCurve25519SharedSecKey(priKey: Uint8Array, pubKey: Uint8Array): Uint8Array {
return tweetnacl.scalarMult(priKey, pubKey);
assert(priKey.length === 32, "bad private key size");
assert(pubKey.length === 32, "bad public key size");
try {
return crypto.diffieHellman({
privateKey: privateKeyObject(X25519_PKCS8_PREFIX, priKey),
publicKey: publicKeyObject(X25519_SPKI_PREFIX, pubKey),
});
} catch (error) {
// OpenSSL refuses a public key of small order, whose shared secret is all zeros; tweetnacl returned the zeros.
// A paired peer can still complete pair verify by signing with its long-term key, but the resulting
// session keys are derived from a publicly known zero secret; authentication does not preserve secrecy here.
if ((error as NodeJS.ErrnoException).code === "ERR_OSSL_FAILED_DURING_DERIVATION") {
return Buffer.alloc(32);
}
throw error;
}
}

/**
* @group Cryptography
*/
export function generateEd25519KeyPair(): Ed25519KeyPair {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const rawPublicKey = rawKey(publicKey);
return { publicKey: rawPublicKey, secretKey: Buffer.concat([rawKey(privateKey), rawPublicKey]) };
}

/**
* Signs with the 64-byte secret key of an {@link Ed25519KeyPair}.
*
* @group Cryptography
*/
export function ed25519Sign(message: Uint8Array, secretKey: Uint8Array): Buffer {
assert(secretKey.length === 64, "bad secret key size");
return crypto.sign(null, message, privateKeyObject(ED25519_PKCS8_PREFIX, secretKey.subarray(0, 32)));
}

/**
* False also for a signature or public key of the wrong size, and for a public key that is no point on the curve.
*
* @group Cryptography
*/
export function ed25519Verify(message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean {
if (signature.length !== 64 || publicKey.length !== 32) {
return false;
}
try {
return crypto.verify(null, message, publicKeyObject(ED25519_SPKI_PREFIX, publicKey), signature);
} catch (error) {
return false;
}
}

/**
Expand Down
Loading