171 lines
6.2 KiB
TypeScript
171 lines
6.2 KiB
TypeScript
|
|
import { describe, test, expect } from 'bun:test';
|
||
|
|
import {
|
||
|
|
toBase64, fromBase64,
|
||
|
|
serializeIdentityKeyPair, deserializeIdentityKeyPair,
|
||
|
|
serializeSignedPreKey, deserializeSignedPreKey,
|
||
|
|
serializeOneTimePreKey, deserializeOneTimePreKey,
|
||
|
|
serializeSessionState, deserializeSessionState,
|
||
|
|
} from '../src/serialization.js';
|
||
|
|
import type { IdentityKeyPair, SignedPreKey, OneTimePreKey, SessionState } from '../src/types.js';
|
||
|
|
|
||
|
|
function randBytes(n: number): Uint8Array {
|
||
|
|
const buf = new Uint8Array(n);
|
||
|
|
crypto.getRandomValues(buf);
|
||
|
|
return buf;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('Serialization', () => {
|
||
|
|
describe('base64', () => {
|
||
|
|
test('roundtrip for various lengths', () => {
|
||
|
|
for (const len of [0, 1, 12, 32, 64, 256]) {
|
||
|
|
const buf = randBytes(len);
|
||
|
|
expect(fromBase64(toBase64(buf))).toEqual(buf);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('IdentityKeyPair', () => {
|
||
|
|
test('roundtrip', () => {
|
||
|
|
const ikp: IdentityKeyPair = {
|
||
|
|
signingPublicKey: randBytes(32),
|
||
|
|
signingPrivateKey: randBytes(32),
|
||
|
|
dhPublicKey: randBytes(32),
|
||
|
|
dhPrivateKey: randBytes(32),
|
||
|
|
};
|
||
|
|
const json = serializeIdentityKeyPair(ikp);
|
||
|
|
const restored = deserializeIdentityKeyPair(json);
|
||
|
|
expect(restored.signingPublicKey).toEqual(ikp.signingPublicKey);
|
||
|
|
expect(restored.signingPrivateKey).toEqual(ikp.signingPrivateKey);
|
||
|
|
expect(restored.dhPublicKey).toEqual(ikp.dhPublicKey);
|
||
|
|
expect(restored.dhPrivateKey).toEqual(ikp.dhPrivateKey);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('SignedPreKey', () => {
|
||
|
|
test('roundtrip', () => {
|
||
|
|
const spk: SignedPreKey = {
|
||
|
|
keyId: 42,
|
||
|
|
keyPair: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
signature: randBytes(64),
|
||
|
|
timestamp: Date.now(),
|
||
|
|
};
|
||
|
|
const restored = deserializeSignedPreKey(serializeSignedPreKey(spk));
|
||
|
|
expect(restored.keyId).toBe(42);
|
||
|
|
expect(restored.keyPair.publicKey).toEqual(spk.keyPair.publicKey);
|
||
|
|
expect(restored.signature).toEqual(spk.signature);
|
||
|
|
expect(restored.timestamp).toBe(spk.timestamp);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('OneTimePreKey', () => {
|
||
|
|
test('roundtrip', () => {
|
||
|
|
const otpk: OneTimePreKey = {
|
||
|
|
keyId: 100,
|
||
|
|
keyPair: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
};
|
||
|
|
const restored = deserializeOneTimePreKey(serializeOneTimePreKey(otpk));
|
||
|
|
expect(restored.keyId).toBe(100);
|
||
|
|
expect(restored.keyPair.publicKey).toEqual(otpk.keyPair.publicKey);
|
||
|
|
expect(restored.keyPair.privateKey).toEqual(otpk.keyPair.privateKey);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('SessionState', () => {
|
||
|
|
test('roundtrip with all fields', () => {
|
||
|
|
const state: SessionState = {
|
||
|
|
remoteIdentityKey: randBytes(32),
|
||
|
|
rootKey: randBytes(32),
|
||
|
|
sendChain: { chainKey: randBytes(32), counter: 5 },
|
||
|
|
receiveChain: { chainKey: randBytes(32), counter: 3 },
|
||
|
|
dhSend: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
dhReceive: randBytes(32),
|
||
|
|
previousSendCounter: 4,
|
||
|
|
skippedKeys: new Map([
|
||
|
|
['abc:1', randBytes(32)],
|
||
|
|
['def:2', randBytes(32)],
|
||
|
|
]),
|
||
|
|
};
|
||
|
|
|
||
|
|
const restored = deserializeSessionState(serializeSessionState(state));
|
||
|
|
expect(restored.remoteIdentityKey).toEqual(state.remoteIdentityKey);
|
||
|
|
expect(restored.rootKey).toEqual(state.rootKey);
|
||
|
|
expect(restored.sendChain.chainKey).toEqual(state.sendChain.chainKey);
|
||
|
|
expect(restored.sendChain.counter).toBe(5);
|
||
|
|
expect(restored.receiveChain!.chainKey).toEqual(state.receiveChain!.chainKey);
|
||
|
|
expect(restored.receiveChain!.counter).toBe(3);
|
||
|
|
expect(restored.dhSend.publicKey).toEqual(state.dhSend.publicKey);
|
||
|
|
expect(restored.dhReceive).toEqual(state.dhReceive);
|
||
|
|
expect(restored.previousSendCounter).toBe(4);
|
||
|
|
expect(restored.skippedKeys.size).toBe(2);
|
||
|
|
expect(restored.skippedKeys.get('abc:1')).toEqual(state.skippedKeys.get('abc:1'));
|
||
|
|
expect(restored.skippedKeys.get('def:2')).toEqual(state.skippedKeys.get('def:2'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('roundtrip with null receiveChain and dhReceive', () => {
|
||
|
|
const state: SessionState = {
|
||
|
|
remoteIdentityKey: randBytes(32),
|
||
|
|
rootKey: randBytes(32),
|
||
|
|
sendChain: { chainKey: randBytes(32), counter: 0 },
|
||
|
|
receiveChain: null,
|
||
|
|
dhSend: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
dhReceive: null,
|
||
|
|
previousSendCounter: 0,
|
||
|
|
skippedKeys: new Map(),
|
||
|
|
};
|
||
|
|
|
||
|
|
const restored = deserializeSessionState(serializeSessionState(state));
|
||
|
|
expect(restored.receiveChain).toBeNull();
|
||
|
|
expect(restored.dhReceive).toBeNull();
|
||
|
|
expect(restored.skippedKeys.size).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('roundtrip preserves __x3dh metadata', () => {
|
||
|
|
const state: SessionState = {
|
||
|
|
remoteIdentityKey: randBytes(32),
|
||
|
|
rootKey: randBytes(32),
|
||
|
|
sendChain: { chainKey: randBytes(32), counter: 0 },
|
||
|
|
receiveChain: null,
|
||
|
|
dhSend: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
dhReceive: null,
|
||
|
|
previousSendCounter: 0,
|
||
|
|
skippedKeys: new Map(),
|
||
|
|
};
|
||
|
|
|
||
|
|
const ephKey = randBytes(32);
|
||
|
|
const idKey = randBytes(32);
|
||
|
|
(state as any).__x3dh = {
|
||
|
|
ephemeralPublicKey: ephKey,
|
||
|
|
signedPreKeyId: 1,
|
||
|
|
preKeyId: 100,
|
||
|
|
identityDHKey: idKey,
|
||
|
|
registrationId: 42,
|
||
|
|
};
|
||
|
|
|
||
|
|
const restored = deserializeSessionState(serializeSessionState(state));
|
||
|
|
const x3dh = (restored as any).__x3dh;
|
||
|
|
expect(x3dh).toBeDefined();
|
||
|
|
expect(x3dh.ephemeralPublicKey).toEqual(ephKey);
|
||
|
|
expect(x3dh.signedPreKeyId).toBe(1);
|
||
|
|
expect(x3dh.preKeyId).toBe(100);
|
||
|
|
expect(x3dh.identityDHKey).toEqual(idKey);
|
||
|
|
expect(x3dh.registrationId).toBe(42);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('roundtrip without __x3dh', () => {
|
||
|
|
const state: SessionState = {
|
||
|
|
remoteIdentityKey: randBytes(32),
|
||
|
|
rootKey: randBytes(32),
|
||
|
|
sendChain: { chainKey: randBytes(32), counter: 0 },
|
||
|
|
receiveChain: null,
|
||
|
|
dhSend: { publicKey: randBytes(32), privateKey: randBytes(32) },
|
||
|
|
dhReceive: null,
|
||
|
|
previousSendCounter: 0,
|
||
|
|
skippedKeys: new Map(),
|
||
|
|
};
|
||
|
|
|
||
|
|
const restored = deserializeSessionState(serializeSessionState(state));
|
||
|
|
expect((restored as any).__x3dh).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|