Files

143 lines
5.4 KiB
TypeScript
Raw Permalink Normal View History

import { describe, test, expect } from 'bun:test';
import { createPrekeyServer, MemoryPrekeyStore, PrekeyServerEvents, signPayload } from '../src/index.js';
import type { PrekeyServerEvent } from '../src/index.js';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import { generateIdentityKeyPair } from '@shade/core';
const crypto = new SubtleCryptoProvider();
function b64(bytes: Uint8Array): string {
return Buffer.from(bytes).toString('base64');
}
function randBytes(n: number): Uint8Array {
const buf = new Uint8Array(n);
globalThis.crypto.getRandomValues(buf);
return buf;
}
describe('PrekeyServerEvents integration', () => {
test('emits events for register, fetch, replenish, delete', async () => {
const events = new PrekeyServerEvents();
const received: PrekeyServerEvent[] = [];
events.on((e) => received.push(e));
const store = new MemoryPrekeyStore();
const app = createPrekeyServer({ crypto, store, disableRateLimit: true, events });
const alice = await generateIdentityKeyPair(crypto);
// Register
const regBody = await signPayload(crypto, alice.signingPrivateKey, {
address: 'alice',
identitySigningKey: b64(alice.signingPublicKey),
identityDHKey: b64(alice.dhPublicKey),
signedPreKey: { keyId: 1, publicKey: b64(randBytes(32)), signature: b64(randBytes(64)) },
oneTimePreKeys: [{ keyId: 100, publicKey: b64(randBytes(32)) }],
});
await app.request('/v1/keys/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(regBody),
});
// Fetch bundle
await app.request('/v1/keys/bundle/alice');
// Replenish
const replenishBody = await signPayload(crypto, alice.signingPrivateKey, {
address: 'alice',
oneTimePreKeys: [
{ keyId: 200, publicKey: b64(randBytes(32)) },
{ keyId: 201, publicKey: b64(randBytes(32)) },
],
});
await app.request('/v1/keys/replenish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(replenishBody),
});
// Delete
const delBody = await signPayload(crypto, alice.signingPrivateKey, { address: 'alice' });
await app.request('/v1/keys/alice', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(delBody),
});
const names = received.map((e) => e.name);
expect(names).toContain('server.identity_registered');
expect(names).toContain('server.bundle_fetched');
expect(names).toContain('server.prekeys_replenished');
expect(names).toContain('server.identity_deleted');
// Verify hadOneTimePreKey is true on the fetch event
const fetchEvent = received.find((e) => e.name === 'server.bundle_fetched');
expect((fetchEvent!.data as any).hadOneTimePreKey).toBe(true);
// Verify replenish reports the right count
const replenishEvent = received.find((e) => e.name === 'server.prekeys_replenished');
expect((replenishEvent!.data as any).count).toBe(2);
expect((replenishEvent!.data as any).totalAfter).toBe(2); // 1 - 1 (consumed) + 2 = 2
});
test('emits server.rate_limited when limits trip', async () => {
const events = new PrekeyServerEvents();
const received: PrekeyServerEvent[] = [];
events.on((e) => received.push(e));
// Rate limit ENABLED for this test
const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore(), events });
// Burn the register limit (5/hour)
for (let i = 0; i < 7; i++) {
const id = await generateIdentityKeyPair(crypto);
const body = await signPayload(crypto, id.signingPrivateKey, {
address: `user${i}`,
identitySigningKey: b64(id.signingPublicKey),
identityDHKey: b64(id.dhPublicKey),
signedPreKey: { keyId: 1, publicKey: b64(randBytes(32)), signature: b64(randBytes(64)) },
});
await app.request('/v1/keys/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-forwarded-for': '203.0.113.99' },
body: JSON.stringify(body),
});
}
const rateLimitedEvents = received.filter((e) => e.name === 'server.rate_limited');
expect(rateLimitedEvents.length).toBeGreaterThan(0);
});
test('SECURITY: no key material in server event payloads', async () => {
const events = new PrekeyServerEvents();
const received: PrekeyServerEvent[] = [];
events.on((e) => received.push(e));
const app = createPrekeyServer({ crypto, store: new MemoryPrekeyStore(), disableRateLimit: true, events });
const alice = await generateIdentityKeyPair(crypto);
const regBody = await signPayload(crypto, alice.signingPrivateKey, {
address: 'alice',
identitySigningKey: b64(alice.signingPublicKey),
identityDHKey: b64(alice.dhPublicKey),
signedPreKey: { keyId: 1, publicKey: b64(randBytes(32)), signature: b64(randBytes(64)) },
oneTimePreKeys: [{ keyId: 100, publicKey: b64(randBytes(32)) }],
});
await app.request('/v1/keys/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(regBody),
});
await app.request('/v1/keys/bundle/alice');
const json = JSON.stringify(received);
// Same regex as core: no 32+ byte base64 or 32+ char hex
const longBase64 = /[A-Za-z0-9+/]{43,}={0,2}/g;
const longHex = /[0-9a-f]{32,}/gi;
expect(json.match(longBase64) ?? []).toEqual([]);
expect(json.match(longHex) ?? []).toEqual([]);
});
});