203 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
});
|