Files
meshcore.ts/test/crypto.test.ts
2026-03-12 20:56:04 +01:00

203 lines
6.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { bytesToHex } from "@hamradio/packet";
import { PublicKey, PrivateKey, SharedSecret, StaticSecret } from "../src/crypto";
const randomBytes = (len: number) => Uint8Array.from({ length: len }, () => Math.floor(Math.random() * 256));
describe("PublicKey", () => {
const keyBytes = randomBytes(32);
const keyHex = bytesToHex(keyBytes);
it("constructs from Uint8Array", () => {
const pk = new PublicKey(keyBytes);
expect(pk.toBytes()).toEqual(keyBytes);
});
it("constructs from string", () => {
const pk = new PublicKey(keyHex);
expect(pk.toBytes()).toEqual(keyBytes);
});
it("throws on invalid constructor input", () => {
// @ts-expect-error testing invalid input
expect(() => new PublicKey(123)).toThrow();
});
it("toHash returns a NodeHash", () => {
const pk = new PublicKey(keyBytes);
expect(typeof pk.toHash()).toBe("number");
});
it("toString returns hex", () => {
const pk = new PublicKey(keyBytes);
expect(pk.toString()).toBe(keyHex);
});
it("equals works for PublicKey, Uint8Array, and string", () => {
const pk = new PublicKey(keyBytes);
expect(pk.equals(pk)).toBe(true);
expect(pk.equals(keyBytes)).toBe(true);
expect(pk.equals(keyHex)).toBe(true);
expect(pk.equals(randomBytes(32))).toBe(false);
});
it("throws on equals with invalid type", () => {
const pk = new PublicKey(keyBytes);
// @ts-expect-error testing invalid input
expect(() => pk.equals(123)).toThrow();
});
it("verify returns false for invalid signature", () => {
const pk = new PublicKey(keyBytes);
expect(pk.verify(new Uint8Array([1, 2, 3]), randomBytes(64))).toBe(false);
});
it("throws on verify with wrong signature length", () => {
const pk = new PublicKey(keyBytes);
expect(() => pk.verify(new Uint8Array([1, 2, 3]), randomBytes(10))).toThrow();
});
});
describe("PrivateKey", () => {
const seed = randomBytes(32);
it("constructs from Uint8Array", () => {
const sk = new PrivateKey(seed);
expect(sk.toPublicKey()).toBeInstanceOf(PublicKey);
});
it("constructs from string", () => {
const sk = new PrivateKey(bytesToHex(seed));
expect(sk.toPublicKey()).toBeInstanceOf(PublicKey);
});
it("throws on invalid seed length", () => {
expect(() => new PrivateKey(randomBytes(10))).toThrow();
});
it("sign and verify", () => {
const sk = new PrivateKey(seed);
const pk = sk.toPublicKey();
const msg = new Uint8Array([1, 2, 3]);
const sig = sk.sign(msg);
expect(pk.verify(msg, sig)).toBe(true);
});
it("calculateSharedSecret returns Uint8Array", () => {
const sk1 = new PrivateKey(seed);
const sk2 = PrivateKey.generate();
const pk2 = sk2.toPublicKey();
const secret = sk1.calculateSharedSecret(pk2);
expect(secret).toBeInstanceOf(Uint8Array);
expect(secret.length).toBeGreaterThan(0);
});
it("calculateSharedSecret accepts string and Uint8Array", () => {
const sk1 = new PrivateKey(seed);
const sk2 = PrivateKey.generate();
const pk2 = sk2.toPublicKey();
expect(sk1.calculateSharedSecret(pk2.toBytes())).toBeInstanceOf(Uint8Array);
expect(sk1.calculateSharedSecret(pk2.toString())).toBeInstanceOf(Uint8Array);
});
it("throws on calculateSharedSecret with invalid type", () => {
const sk = new PrivateKey(seed);
// @ts-expect-error testing invalid input
expect(() => sk.calculateSharedSecret(123)).toThrow();
});
it("generate returns PrivateKey", () => {
expect(PrivateKey.generate()).toBeInstanceOf(PrivateKey);
});
});
describe("SharedSecret", () => {
const secret = randomBytes(32);
it("constructs from 32 bytes", () => {
const ss = new SharedSecret(secret);
expect(ss.toBytes()).toEqual(secret);
});
it("pads if 16 bytes", () => {
const short = randomBytes(16);
const ss = new SharedSecret(short);
expect(ss.toBytes().length).toBe(32);
expect(Array.from(ss.toBytes()).slice(16)).toEqual(Array.from(short));
});
it("throws on invalid length", () => {
expect(() => new SharedSecret(randomBytes(10))).toThrow();
});
it("toHash returns number", () => {
const ss = new SharedSecret(secret);
expect(typeof ss.toHash()).toBe("number");
});
it("toString returns hex", () => {
const ss = new SharedSecret(secret);
expect(ss.toString()).toBe(bytesToHex(secret));
});
it("encrypt and decrypt roundtrip", () => {
const ss = new SharedSecret(secret);
const data = new Uint8Array([1, 2, 3, 4, 5]);
const { hmac, ciphertext } = ss.encrypt(data);
const decrypted = ss.decrypt(hmac, ciphertext);
expect(Array.from(decrypted.slice(0, data.length))).toEqual(Array.from(data));
});
it("throws on decrypt with wrong hmac", () => {
const ss = new SharedSecret(secret);
const data = new Uint8Array([1, 2, 3]);
const { ciphertext } = ss.encrypt(data);
expect(() => ss.decrypt(new Uint8Array([0, 0]), ciphertext)).toThrow();
});
it("throws on decrypt with wrong hmac length", () => {
const ss = new SharedSecret(secret);
expect(() => ss.decrypt(new Uint8Array([1]), new Uint8Array([1, 2, 3]))).toThrow();
});
it('fromName "Public"', () => {
const ss = SharedSecret.fromName("Public");
expect(ss).toBeInstanceOf(SharedSecret);
});
it("fromName with #group", () => {
const ss = SharedSecret.fromName("#group");
expect(ss).toBeInstanceOf(SharedSecret);
});
it("fromName throws on invalid name", () => {
expect(() => SharedSecret.fromName("foo")).toThrow();
});
});
describe("StaticSecret", () => {
const secret = randomBytes(32);
it("constructs from Uint8Array", () => {
const ss = new StaticSecret(secret);
expect(ss.publicKey()).toBeInstanceOf(PublicKey);
});
it("constructs from string", () => {
const ss = new StaticSecret(bytesToHex(secret));
expect(ss.publicKey()).toBeInstanceOf(PublicKey);
});
it("throws on invalid length", () => {
expect(() => new StaticSecret(randomBytes(10))).toThrow();
});
it("diffieHellman returns SharedSecret", () => {
const ss1 = new StaticSecret(secret);
const ss2 = new StaticSecret(randomBytes(32));
const pk2 = ss2.publicKey();
const shared = ss1.diffieHellman(pk2);
expect(shared).toBeInstanceOf(SharedSecret);
});
});