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); }); });