143 lines
5.4 KiB
TypeScript
143 lines
5.4 KiB
TypeScript
|
|
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([]);
|
||
|
|
});
|
||
|
|
});
|