// PHASE 0 #2 — MEMBERS-ONLY ENCRYPTION. The base encryption key is derived from the roomSeed (the
// members-only secret), so co-owners transparently share it, and a non-member — who has neither the
// roomSeed nor the key — cannot read the co-owned content even if the (public-ish) base key leaks.
import test from 'brittle'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import crypto from 'hypercore-crypto'
import { deriveAtprotoKey, createAtprotoIdentity } from 'atproto-identity'
import { AtprotoBase } from '../lib/base.js'
const MNEMONIC = 'test test test test test test test test test test test junk'
const ID = createAtprotoIdentity(MNEMONIC, { handle: 'x.7rnx.net', pdsUrl: 'https://pds.7rnx.net' })
const KEY = deriveAtprotoKey(MNEMONIC)
const post = (text) => ({ $type: 'app.bsky.feed.post', text, createdAt: '2026-07-23T00:00:00.000Z', langs: ['en'] })
const hex = (b) => b.toString('hex')
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
function mk (opts) { const dir = mkdtempSync(join(tmpdir(), 'enc-')); const base = new AtprotoBase({ path: dir, did: ID.did, privateKey: KEY.privateKey, ackInterval: 100, ...opts }); return { base, dir, close: async () => { try { await base.close() } catch {} ; rmSync(dir, { recursive: true, force: true }) } } }
const pipe = (a, b) => { const s1 = a.base.base.replicate(true); const s2 = b.base.base.replicate(false); s1.pipe(s2).pipe(s1); return () => { try { s1.destroy() } catch {} ; try { s2.destroy() } catch {} } }
async function settle (...ws) { for (let i = 0; i < 20; i++) { for (const w of ws) { try { await w.base.base.update() } catch {} } await sleep(60) } }
test('encryption: co-owners derive the identical members-only key from the roomSeed', async (t) => {
const roomSeed = Buffer.alloc(32, 5)
const idA = crypto.keyPair(); const idB = crypto.keyPair()
const roster = [{ key: hex(idA.publicKey), role: 1 }, { key: hex(idB.publicKey), role: 1 }]
const A = mk({ roomSeed, identity: idA, members: roster }); const B = mk({ roomSeed, identity: idB, members: roster })
await A.base.ready(); await B.base.ready()
t.teardown(async () => { await A.close(); await B.close() })
t.ok(A.base._encryptionKey && B.base._encryptionKey, 'both derived an encryption key from the roomSeed')
t.is(A.base._encryptionKey.toString('hex'), B.base._encryptionKey.toString('hex'), 'the derived members-only key is identical')
t.absent(A.base._encryptionKey.equals(roomSeed), 'the encryption key is DERIVED (hashed), not the raw roomSeed')
})
test('encryption: a non-member with the base key but not the roomSeed CANNOT read the content', async (t) => {
const roomSeed = Buffer.alloc(32, 5)
const idA = crypto.keyPair()
const A = mk({ roomSeed, identity: idA, members: [{ key: hex(idA.publicKey), role: 1 }] })
await A.base.ready()
await A.base.putRecord('app.bsky.feed.post', '3secret', post('members only'))
await A.base.settled()
t.is((await A.base.indexedRecords()).length, 1, 'A (a member) reads its own record')
// X: knows the base key (as if leaked) but NOT the roomSeed → a wrong/foreign encryption key.
const idX = crypto.keyPair()
const X = mk({ bootstrap: A.base.base.key, encryptionKey: Buffer.alloc(32, 99), identity: idX })
let xRecords = 'unopened'; let un = null
try { await X.base.ready(); un = pipe(A, X); await settle(A, X); xRecords = await X.base.indexedRecords() } catch { xRecords = 'error' }
t.teardown(async () => { if (un) un(); await A.close(); await X.close() })
const leaked = Array.isArray(xRecords) && xRecords.some((r) => r && r.value && r.value.text === 'members only')
t.absent(leaked, 'the non-member could NOT read the members-only record (encryption gates content even with the base key)')
})