import { describe, it, expect, beforeEach } from 'vitest'; import { Identity, LocalIdentity, Contact, Group, Contacts, parseNodeHash } from './identity'; import { PrivateKey, PublicKey, SharedSecret } from './crypto'; import { DecryptedGroupText, DecryptedGroupData } from './packet.types'; import { bytesToHex } from './parser'; function randomBytes(len: number) { return Uint8Array.from({ length: len }, () => Math.floor(Math.random() * 256)); } describe('parseNodeHash', () => { it('parses Uint8Array', () => { expect(parseNodeHash(Uint8Array.of(0x42))).toBe(0x42); }); it('parses number', () => { expect(parseNodeHash(0x42)).toBe(0x42); expect(() => parseNodeHash(-1)).toThrow(); expect(() => parseNodeHash(256)).toThrow(); }); it('parses string', () => { expect(parseNodeHash('2a')).toBe(0x2a); expect(() => parseNodeHash('2a2a')).toThrow(); }); it('throws on invalid type', () => { // @ts-expect-error expect(() => parseNodeHash({})).toThrow(); }); }); describe('Identity', () => { let pub: Uint8Array; let identity: Identity; beforeEach(() => { pub = randomBytes(32); identity = new Identity(pub); }); it('constructs from Uint8Array', () => { expect(identity.publicKey.toBytes()).toEqual(pub); }); it('constructs from string', () => { const hex = bytesToHex(pub); const id = new Identity(hex); expect(id.publicKey.toBytes()).toEqual(pub); }); it('hash returns NodeHash', () => { expect(typeof identity.hash()).toBe('number'); }); it('toString returns string', () => { expect(typeof identity.toString()).toBe('string'); }); it('verify delegates to publicKey', () => { const msg = randomBytes(10); const sig = randomBytes(64); expect(identity.verify(sig, msg)).toBe(identity.publicKey.verify(msg, sig)); }); it('matches works for various types', () => { expect(identity.matches(identity)).toBe(true); expect(identity.matches(identity.publicKey)).toBe(true); expect(identity.matches(identity.publicKey.toBytes())).toBe(true); expect(identity.matches(identity.publicKey.toString())).toBe(true); expect(identity.matches(randomBytes(32))).toBe(false); }); }); describe('LocalIdentity', () => { let priv: PrivateKey; let pub: PublicKey; let local: LocalIdentity; beforeEach(() => { priv = PrivateKey.generate(); pub = priv.toPublicKey(); local = new LocalIdentity(priv, pub); }); it('constructs from PrivateKey and PublicKey', () => { expect(local.publicKey.toBytes()).toEqual(pub.toBytes()); }); it('constructs from Uint8Array and string', () => { const priv2 = PrivateKey.generate(); const pub2 = priv2.toPublicKey(); const local2 = new LocalIdentity(priv2.toBytes(), pub2.toString()); expect(local2.publicKey.toBytes()).toEqual(pub2.toBytes()); }); it('signs message', () => { const msg = randomBytes(12); const sig = local.sign(msg); expect(sig).toBeInstanceOf(Uint8Array); expect(sig.length).toBeGreaterThan(0); }); it('calculates shared secret with Identity', () => { const otherPriv = PrivateKey.generate(); const other = new Identity(otherPriv.toPublicKey().toBytes()); const secret = local.calculateSharedSecret(other); expect(secret).toBeInstanceOf(SharedSecret); }); it('calculates shared secret with IPublicKey', () => { const otherPriv = PrivateKey.generate(); const otherPub = otherPriv.toPublicKey(); const secret = local.calculateSharedSecret(otherPub); expect(secret).toBeInstanceOf(SharedSecret); }); }); describe('Contact', () => { let priv: PrivateKey; let pub: PublicKey; let identity: Identity; let contact: Contact; beforeEach(() => { priv = PrivateKey.generate(); pub = priv.toPublicKey(); identity = new Identity(pub.toBytes()); contact = new Contact('Alice', identity); }); it('constructs from Identity', () => { expect(contact.identity).toBe(identity); expect(contact.name).toBe('Alice'); }); it('constructs from Uint8Array', () => { const c = new Contact('Bob', pub.toBytes()); expect(c.identity.publicKey.toBytes()).toEqual(pub.toBytes()); }); it('matches works', () => { expect(contact.matches(pub)).toBe(true); expect(contact.matches(pub.toBytes())).toBe(true); expect(contact.matches(new PublicKey(randomBytes(32)))).toBe(false); }); it('publicKey returns PublicKey', () => { expect(contact.publicKey()).toBeInstanceOf(PublicKey); }); it('calculateSharedSecret delegates', () => { const me = new LocalIdentity(priv, pub); const secret = contact.calculateSharedSecret(me); expect(secret).toBeInstanceOf(SharedSecret); }); }); describe('Group', () => { let group: Group; let secret: SharedSecret; let name: string; beforeEach(() => { name = '#test'; secret = SharedSecret.fromName(name); group = new Group(name, secret); }); it('constructs with and without secret', () => { const g1 = new Group(name, secret); expect(g1).toBeInstanceOf(Group); const g2 = new Group(name); expect(g2).toBeInstanceOf(Group); }); it('hash returns NodeHash', () => { expect(typeof group.hash()).toBe('number'); }); it('encryptText and decryptText roundtrip', () => { const plain: DecryptedGroupText = { timestamp: new Date(), textType: 1, attempt: 2, message: 'hello' }; const { hmac, ciphertext } = group.encryptText(plain); const decrypted = group.decryptText(hmac, ciphertext); expect(decrypted.message).toBe(plain.message); expect(decrypted.textType).toBe(plain.textType); expect(decrypted.attempt).toBe(plain.attempt); expect(typeof decrypted.timestamp).toBe('object'); }); it('encryptData and decryptData roundtrip', () => { const plain: DecryptedGroupData = { timestamp: new Date(), data: randomBytes(10) }; const { hmac, ciphertext } = group.encryptData(plain); const decrypted = group.decryptData(hmac, ciphertext); expect(decrypted.data).toEqual(plain.data); expect(typeof decrypted.timestamp).toBe('object'); }); it('decryptText throws on short ciphertext', () => { expect(() => group.decryptText(randomBytes(16), Uint8Array.of(1, 2, 3, 4))).toThrow(); }); it('decryptData throws on short ciphertext', () => { expect(() => group.decryptData(randomBytes(16), Uint8Array.of(1, 2, 3))).toThrow(); }); }); describe('Contacts', () => { let contacts: Contacts; let local: LocalIdentity; let contact: Contact; let group: Group; beforeEach(() => { contacts = new Contacts(); const priv = PrivateKey.generate(); const pub = priv.toPublicKey(); local = new LocalIdentity(priv, pub); contact = new Contact('Alice', pub.toBytes()); group = new Group('Public'); }); it('addLocalIdentity and addContact', () => { contacts.addLocalIdentity(local); contacts.addContact(contact); // No error means success }); it('addGroup', () => { contacts.addGroup(group); }); it('decryptGroupText and decryptGroupData', () => { contacts.addGroup(group); const text: DecryptedGroupText = { timestamp: new Date(), textType: 1, attempt: 0, message: 'hi' }; const { hmac, ciphertext } = group.encryptText(text); const res = contacts.decryptGroupText(group.hash(), hmac, ciphertext); expect(res.decrypted.message).toBe(text.message); const data: DecryptedGroupData = { timestamp: new Date(), data: randomBytes(8) }; const enc = group.encryptData(data); const res2 = contacts.decryptGroupData(group.hash(), enc.hmac, enc.ciphertext); expect(res2.decrypted.data).toEqual(data.data); }); it('decryptGroupText throws on unknown group', () => { expect(() => contacts.decryptGroupText(0x99, randomBytes(16), randomBytes(16))).toThrow(); }); it('decryptGroupData throws on unknown group', () => { expect(() => contacts.decryptGroupData(0x99, randomBytes(16), randomBytes(16))).toThrow(); }); it('decrypt throws on unknown source/destination', () => { contacts.addLocalIdentity(local); expect(() => contacts.decrypt(0x99, 0x99, randomBytes(16), randomBytes(16)) ).toThrow(); }); it('decrypt throws on decryption failure', () => { contacts.addLocalIdentity(local); contacts.addContact(contact); expect(() => contacts.decrypt(contact.identity.hash(), local.publicKey.key[0], randomBytes(16), randomBytes(16)) ).toThrow(); }); it('decrypt works for valid shared secret', () => { // Setup two identities that can communicate const privA = PrivateKey.generate(); const pubA = privA.toPublicKey(); const localA = new LocalIdentity(privA, pubA); const privB = PrivateKey.generate(); const pubB = privB.toPublicKey(); const contactB = new Contact('Bob', pubB); contacts.addLocalIdentity(localA); contacts.addContact(contactB); // Encrypt a message using the shared secret const shared = localA.calculateSharedSecret(contactB.identity); const msg = randomBytes(12); const { hmac, ciphertext } = shared.encrypt(msg); const srcHash = contactB.identity.hash(); const dstHash = localA.publicKey.key[0]; const res = contacts.decrypt(srcHash, dstHash, hmac, ciphertext); expect(res.decrypted).toEqual(msg); expect(res.contact.name).toBe('Bob'); expect(res.localIdentity.publicKey.toBytes()).toEqual(pubA.toBytes()); }); });