203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { PublicKey, PrivateKey, SharedSecret, StaticSecret } from '../src/crypto';
|
|
import { bytesToHex, hexToBytes } from '../src/parser';
|
|
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
});
|
|
});
|