import { describe, expect, test } from 'bun:test'; import { DOMAIN_BUNDLE, computeBundleHash, encodeLeafData, leafHash, nodeHash, OP_REGISTER, } from '../src/hashes.js'; import { sha256Sync } from '../src/sha256.js'; describe('RFC 6962 hash primitives', () => { test('leafHash applies 0x00 prefix', () => { const data = new Uint8Array([1, 2, 3]); const expected = sha256Sync(new Uint8Array([0x00, 1, 2, 3])); expect(leafHash(data)).toEqual(expected); }); test('nodeHash applies 0x01 prefix', () => { const left = new Uint8Array(32).fill(0xaa); const right = new Uint8Array(32).fill(0xbb); const concat = new Uint8Array(1 + 32 + 32); concat[0] = 0x01; concat.set(left, 1); concat.set(right, 33); const expected = sha256Sync(concat); expect(nodeHash(left, right)).toEqual(expected); }); test('leafHash and nodeHash never collide', () => { // Same content but different domain → different hash const x = new Uint8Array(32).fill(0x42); const lh = leafHash(x); const concat = new Uint8Array(64).fill(0x42); const nh = nodeHash(concat.slice(0, 32), concat.slice(32)); expect(Buffer.from(lh).toString('hex')).not.toBe(Buffer.from(nh).toString('hex')); }); }); describe('encodeLeafData', () => { test('encodes timestamp + operation + address + bundleHash', () => { const buf = encodeLeafData( 0x010203040506, OP_REGISTER, 'alice', new Uint8Array([0xde, 0xad, 0xbe, 0xef]), ); // 8 + 1 + 2 + 5 + 2 + 4 = 22 expect(buf.length).toBe(22); // last 4 bytes = bundleHash expect(buf[buf.length - 4]).toBe(0xde); expect(buf[buf.length - 1]).toBe(0xef); }); test('rejects address > 65535 bytes', () => { const huge = 'a'.repeat(0x10000); expect(() => encodeLeafData(0, OP_REGISTER, huge, new Uint8Array(0))).toThrow(); }); }); describe('computeBundleHash', () => { test('deterministic over equal input', () => { const sk = new Uint8Array(32).fill(0x11); const dk = new Uint8Array(32).fill(0x22); const pk = new Uint8Array(32).fill(0x33); const sig = new Uint8Array(64).fill(0x44); const a = computeBundleHash({ identitySigningKey: sk, identityDHKey: dk, signedPreKey: { keyId: 7, publicKey: pk, signature: sig }, }); const b = computeBundleHash({ identitySigningKey: sk, identityDHKey: dk, signedPreKey: { keyId: 7, publicKey: pk, signature: sig }, }); expect(a).toEqual(b); }); test('changing keyId changes hash', () => { const sk = new Uint8Array(32).fill(0x11); const dk = new Uint8Array(32).fill(0x22); const pk = new Uint8Array(32).fill(0x33); const sig = new Uint8Array(64).fill(0x44); const a = computeBundleHash({ identitySigningKey: sk, identityDHKey: dk, signedPreKey: { keyId: 1, publicKey: pk, signature: sig }, }); const b = computeBundleHash({ identitySigningKey: sk, identityDHKey: dk, signedPreKey: { keyId: 2, publicKey: pk, signature: sig }, }); expect(a).not.toEqual(b); }); test('rejects wrong-length keys', () => { expect(() => computeBundleHash({ identitySigningKey: new Uint8Array(31), identityDHKey: new Uint8Array(32), signedPreKey: { keyId: 0, publicKey: new Uint8Array(32), signature: new Uint8Array(64), }, }), ).toThrow(); }); test('uses domain prefix 0x01', () => { const sk = new Uint8Array(32); const dk = new Uint8Array(32); const pk = new Uint8Array(32); const sig = new Uint8Array(64); const expected = sha256Sync( Buffer.concat([Buffer.from([DOMAIN_BUNDLE]), sk, dk, Buffer.alloc(4), pk, sig]), ); const got = computeBundleHash({ identitySigningKey: sk, identityDHKey: dk, signedPreKey: { keyId: 0, publicKey: pk, signature: sig }, }); expect(Buffer.from(got).toString('hex')).toBe(Buffer.from(expected).toString('hex')); }); });