/**
* OrbitDB encryption hooks backed by the room's HKDF keys (PROTOCOL.md v2).
*
* `replication` encrypts the ENTIRE log entry (clock, writer key, identity
* ref, signature, links, payload) before it becomes a content-addressed
* block — the relay, bitswap observers, and anyone who merely learns the DB
* address see opaque bytes. `data` additionally encrypts the payload inside
* the entry (future-proofs a semi-trusted pinner that must validate entry
* structure but never read content).
*
* Decrypt THROWS on failure — OrbitDB treats that as "reject this entry",
* which is the whole access-control story: only secret-holders can produce
* entries members accept (write:["*"] + decrypt-or-drop, v1 §3 transplanted).
*/
import type { RoomCrypto, Bytes } from "./crypto";
interface EncryptFns {
encrypt(bytes: Uint8Array): Promise<Uint8Array>;
decrypt(bytes: Uint8Array): Promise<Uint8Array>;
}
export interface LogEncryption {
replication: EncryptFns;
data: EncryptFns;
}
export async function makeLogEncryption(rc: RoomCrypto): Promise<LogEncryption> {
const [entry, payload] = await Promise.all([rc.logEntryCipher(), rc.logPayloadCipher()]);
const wrap = (cipher: { seal(d: Bytes): Promise<Bytes>; open(d: Bytes): Promise<Bytes | null> }): EncryptFns => ({
encrypt: (bytes) => cipher.seal(asBytes(bytes)),
decrypt: async (bytes) => {
const pt = await cipher.open(asBytes(bytes));
if (pt === null) throw new Error("Could not decrypt (not a room member or tampered block)");
return pt;
},
});
return { replication: wrap(entry), data: wrap(payload) };
}
function asBytes(u8: Uint8Array): Bytes {
// dag-cbor hands us views into larger buffers; WebCrypto wants plain ones
if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength && u8.buffer instanceof ArrayBuffer) {
return u8 as Bytes;
}
const copy = new Uint8Array(u8.byteLength);
copy.set(u8);
return copy;
}