Refactoring
This commit is contained in:
308
src/identity.test.ts
Normal file
308
src/identity.test.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user