diff --git a/core/keybag/key-bag.ts b/core/keybag/key-bag.ts index 1ab6b088d..8031143cd 100644 --- a/core/keybag/key-bag.ts +++ b/core/keybag/key-bag.ts @@ -85,11 +85,29 @@ export class KeyBag implements KeyBagIf { ); } + /** + * Ensures a store key is present in the URL, generating one if needed. + * Rejects attempts to use insecure (unencrypted) storage. + * + * @param url - The URI to ensure has a store key + * @param keyFactory - Factory function to generate a key name if none exists + * @returns Result containing the URI with store key, or error if insecure mode attempted + * @throws Error if storekey=insecure is attempted (removed for security) + */ async ensureKeyFromUrl(url: URI, keyFactory: () => string): Promise> { // add storekey to url const storeKey = url.getParam(PARAM.STORE_KEY); if (storeKey === "insecure") { - return Result.Ok(url); + return Result.Err( + this.logger + .Error() + .Msg( + "storekey=insecure is no longer supported. " + + "Data must be encrypted. Remove the storekey=insecure parameter " + + "to use automatic key generation, or provide a valid encryption key.", + ) + .AsError(), + ); } if (!storeKey) { const keyName = `@${keyFactory()}@`; diff --git a/core/runtime/keyed-crypto.ts b/core/runtime/keyed-crypto.ts index d47e903f1..f21382126 100644 --- a/core/runtime/keyed-crypto.ts +++ b/core/runtime/keyed-crypto.ts @@ -153,96 +153,46 @@ class cryptoAction implements CryptoAction { } } -class nullCodec implements AsyncBlockCodec<24, Uint8Array, IvKeyIdData> { - readonly code = 24; - readonly name = "Fireproof@unencrypted-block"; - readonly empty = new Uint8Array(); - - async encode(data: Uint8Array): Promise { - return data; - } - async decode(data: Uint8Array): Promise { - return { - iv: this.empty, - keyId: this.empty, - data: data, - }; - } -} - -class noCrypto implements CryptoAction { - readonly ivLength = 0; - readonly code = 0x0; - readonly name = "Fireproof@unencrypted-block"; - readonly logger: Logger; - readonly crypto: CryptoRuntime; - readonly key: KeysByFingerprint; - readonly isEncrypting = false; - readonly _fingerPrint = "noCrypto:" + Math.random(); - readonly url: URI; - constructor(url: URI, cyrt: CryptoRuntime, sthis: SuperThis) { - this.logger = ensureLogger(sthis, "noCrypto"); - this.crypto = cyrt; - this.key = { - id: sthis.nextId().str, - name: "noCrypto", - get: () => { - throw this.logger.Error().Msg("noCrypto.get not implemented").AsError(); - }, - upsert: () => { - throw this.logger.Error().Msg("noCrypto.upsert not implemented").AsError(); - }, - asV2StorageKeyItem: () => { - throw this.logger.Error().Msg("noCrypto.asV2KeysItem not implemented").AsError(); - }, - }; - this.url = url; - } - - fingerPrint(): Promise { - return Promise.resolve(this._fingerPrint); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - codec(iv?: Uint8Array): AsyncBlockCodec<24, Uint8Array, IvKeyIdData> { - return new nullCodec(); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - algo(iv?: Uint8Array): { name: string; iv: Uint8Array; tagLength: number } { - return { - name: "noCrypto", - iv: new Uint8Array(), - tagLength: 0, - }; - } - _decrypt(): Promise { - throw this.logger.Error().Msg("noCrypto.decrypt not implemented").AsError(); - } - _encrypt(): Promise { - throw this.logger.Error().Msg("noCrypto.decrypt not implemented").AsError(); - } -} - +/** + * Factory function to create a CryptoAction for encrypting/decrypting data. + * Encryption is mandatory - insecure mode is no longer supported. + * + * @param url - URI containing store configuration including store key + * @param kb - KeyBag interface for key management + * @param sthis - SuperThis context for logging and runtime access + * @returns CryptoAction instance configured for encryption + * @throws Error if storekey=insecure is attempted or if key retrieval fails + */ export async function keyedCryptoFactory(url: URI, kb: KeyBagIf, sthis: SuperThis): Promise { const storekey = url.getParam(PARAM.STORE_KEY); - if (storekey && storekey !== "insecure") { + if (storekey === "insecure") { + throw sthis.logger + .Error() + .Str("url", url.toString()) + .Msg( + "storekey=insecure is no longer supported. " + + "Data must be encrypted. Remove the storekey=insecure parameter " + + "to use automatic key generation, or provide a valid encryption key.", + ) + .AsError(); + } + if (storekey) { const rkey = await kb.getNamedKey(storekey, false); if (rkey.isErr()) { - // try { - // rkey = await kb.toKeyWithFingerPrint(storekey); - // } catch (e) { - throw ( - sthis.logger - .Error() - // .Err(e) - .Str("keybag", kb.rt.id()) - // .Result("key", rkey) - .Str("name", storekey) - .Msg("getNamedKey failed") - .AsError() - ); - // } + throw sthis.logger + .Error() + .Str("keybag", kb.rt.id()) + .Str("name", storekey) + .Msg("getNamedKey failed") + .AsError(); } return new cryptoAction(url, rkey.Ok(), kb.rt.crypto, sthis); } - return new noCrypto(url, kb.rt.crypto, sthis); + // No storekey specified - this should not happen in normal operation + // as ensureKeyFromUrl should always add one, but handle gracefully + throw sthis.logger + .Error() + .Str("url", url.toString()) + .Msg("No store key specified. Use ensureKeyFromUrl to add encryption key to URL.") + .AsError(); } diff --git a/core/tests/blockstore/keyed-crypto-indexeddb-file.test.ts b/core/tests/blockstore/keyed-crypto-indexeddb-file.test.ts index 1779308b2..377ceacd6 100644 --- a/core/tests/blockstore/keyed-crypto-indexeddb-file.test.ts +++ b/core/tests/blockstore/keyed-crypto-indexeddb-file.test.ts @@ -121,14 +121,16 @@ describe("KeyedCryptoStore", () => { baseUrl = baseUrl.build().defParam(PARAM.NAME, "test").URI(); loader = mockLoader(sthis); }); - it("no crypto", async () => { + /** + * Tests that creating stores with storekey=insecure is rejected. + * The insecure mode has been removed for security (see spec-03). + */ + it("rejects insecure storekey", async () => { const url = baseUrl.build().setParam(PARAM.STORE_KEY, "insecure").URI(); - for (const pstore of (await createAttachedStores(url, loader, "insecure")).stores.baseStores) { - const store = await pstore; - // await store.start(); - const kc = await store.keyedCrypto(); - expect(kc.constructor.name).toBe("noCrypto"); - // expect(kc.isEncrypting).toBe(false); - } + + // Attempting to create stores with insecure storekey should throw + await expect(createAttachedStores(url, loader, "test-indexeddb-file")).rejects.toThrow( + /storekey=insecure is no longer supported/, + ); }); }); diff --git a/core/tests/blockstore/keyed-crypto.test.ts b/core/tests/blockstore/keyed-crypto.test.ts index 99cc9e4ce..7e284df05 100644 --- a/core/tests/blockstore/keyed-crypto.test.ts +++ b/core/tests/blockstore/keyed-crypto.test.ts @@ -256,22 +256,17 @@ describe("KeyedCryptoStore", () => { kb = await getKeyBag(sthis, {}); loader = mockLoader(sthis); }); - it("no crypto", async () => { + it("rejects insecure storekey", async () => { const url = baseUrl.build().setParam(PARAM.STORE_KEY, "insecure").URI(); - for (const pstore of (await createAttachedStores(url, loader, "insecure")).stores.baseStores) { - const store = await pstore; - // await store.start(); - const kc = await store.keyedCrypto(); - expect(kc.constructor.name).toBe("noCrypto"); - // expect(kc.isEncrypting).toBe(false); - expect(kc.constructor.name).toBe("noCrypto"); - // expect(kc.isEncrypting).toBe(false); - } + // Attempting to create stores with insecure storekey should throw + await expect(createAttachedStores(url, loader, "insecure")).rejects.toThrow( + /storekey=insecure is no longer supported/, + ); }); it("create key", async () => { - for (const pstore of (await createAttachedStores(baseUrl, loader, "insecure")).stores.baseStores) { + for (const pstore of (await createAttachedStores(baseUrl, loader, "test-create-key")).stores.baseStores) { const store = await pstore; // await bs.ensureStart(await pstore, logger); const kc = await store.keyedCrypto(); expect(kc.constructor.name).toBe("cryptoAction"); @@ -286,7 +281,7 @@ describe("KeyedCryptoStore", () => { const key = base58btc.encode(kb.rt.crypto.randomBytes(kb.rt.keyLength)); const genKey = await kb.getNamedKey("@heute@", false, key); const url = baseUrl.build().setParam(PARAM.STORE_KEY, "@heute@").URI(); - for (const pstore of (await createAttachedStores(url, loader, "insecure")).stores.baseStores) { + for (const pstore of (await createAttachedStores(url, loader, "test-key-ref")).stores.baseStores) { const store = await pstore; // await store.start(); expect(store.url().getParam(PARAM.STORE_KEY)).toBe(`@heute@`); @@ -307,7 +302,7 @@ describe("KeyedCryptoStore", () => { it("key", async () => { const key = base58btc.encode(kb.rt.crypto.randomBytes(kb.rt.keyLength)); const url = baseUrl.build().setParam(PARAM.STORE_KEY, key).URI(); - for (const pstore of (await createAttachedStores(url, loader, "insecure")).stores.baseStores) { + for (const pstore of (await createAttachedStores(url, loader, "test-direct-key")).stores.baseStores) { // for (const pstore of [strt.makeDataStore(loader), strt.makeMetaStore(loader), strt.makeWALStore(loader)]) { const store = await pstore; // await store.start(); diff --git a/core/tests/blockstore/standalone.test.ts b/core/tests/blockstore/standalone.test.ts index c03893db0..74f7f4c30 100644 --- a/core/tests/blockstore/standalone.test.ts +++ b/core/tests/blockstore/standalone.test.ts @@ -105,7 +105,6 @@ describe("standalone", () => { default: uri = BuildURI.from("file://dist/standalone") .setParam(PARAM.NAME, "peer-log") - .setParam(PARAM.STORE_KEY, "insecure") .URI(); break; } @@ -115,10 +114,10 @@ describe("standalone", () => { writeQueue: { chunkSize: 32 }, storeUrls: { data: { - meta: uri.build().setParam(PARAM.STORE, "meta").setParam(PARAM.STORE_KEY, "insecure").URI(), - car: uri.build().setParam(PARAM.STORE, "car").setParam(PARAM.STORE_KEY, "insecure").URI(), - file: uri.build().setParam(PARAM.STORE, "file").setParam(PARAM.STORE_KEY, "insecure").URI(), - wal: uri.build().setParam(PARAM.STORE, "wal").setParam(PARAM.STORE_KEY, "insecure").URI(), + meta: uri.build().setParam(PARAM.STORE, "meta").URI(), + car: uri.build().setParam(PARAM.STORE, "car").URI(), + file: uri.build().setParam(PARAM.STORE, "file").URI(), + wal: uri.build().setParam(PARAM.STORE, "wal").URI(), }, }, } as LedgerOpts); diff --git a/core/tests/fireproof/attachable.test.ts b/core/tests/fireproof/attachable.test.ts index 785662f51..1fec8937c 100644 --- a/core/tests/fireproof/attachable.test.ts +++ b/core/tests/fireproof/attachable.test.ts @@ -1,7 +1,5 @@ import { stripper, AppContext, BuildURI, URI, WithoutPromise } from "@adviser/cement"; import { Attachable, Database, fireproof, GatewayUrlsParam, PARAM, Attached, TraceFn } from "@fireproof/core"; -import { CarReader } from "@ipld/car/reader"; -import * as dagCbor from "@ipld/dag-cbor"; import { mockLoader } from "../helpers.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ensureSuperThis, sleep } from "@fireproof/core-runtime"; @@ -107,9 +105,15 @@ describe("meta check", () => { await db1.close(); }); + /** + * Tests that a database with multiple records persists and reopens correctly. + * Verifies carLog structure, memory gateway entries, and data integrity. + * Note: Uses encryption (insecure mode removed per ROBUST-03). + */ it("multiple record Database", async () => { const name = `remote-db-${sthis.nextId().str}`; - const base = `memory://${name}?storekey=insecure`; + // Use encrypted storage (insecure mode no longer supported) + const base = `memory://${name}`; const db = fireproof(name, { storeUrls: { base, @@ -118,40 +122,38 @@ describe("meta check", () => { await db.ready(); await db.put({ _id: `id-${0}`, value: `value-${0}` }); const gws = db.ledger.crdt.blockstore.loader.attachedStores.local(); - expect(db.ledger.crdt.blockstore.loader.carLog.asArray().map((i) => i.map((i) => i.toString()))).toEqual([ - ["baembeieldbalgnyxqp7rmj4cbrot75gweavqy3aw22km43zsfufrihfn7e"], - ["baembeig2is4vdgz4gyiadfh5uutxxeiuqtacnesnytrnilpwcu7q5m5tmu"], - ]); + + // Verify carLog has expected structure (genesis + one write) + const carLog = db.ledger.crdt.blockstore.loader.carLog.asArray(); + expect(carLog.length).toBe(2); + expect(carLog[0].length).toBeGreaterThan(0); + expect(carLog[1].length).toBeGreaterThan(0); + // Each entry should be a valid CID string + carLog.forEach((entry) => { + entry.forEach((cid) => { + expect(cid.toString()).toMatch(/^baem/); + }); + }); + await db.close(); - expect( - Array.from(((gws.active.car.realGateway as DefSerdeGateway).gw as MemoryGateway).memories.entries()) - .filter(([k]) => k.startsWith(`memory://${name}`)) - .map(([k]) => - stripper( - ["name", "storekey", "version"], - Array.from(URI.from(k).getParams).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}), - ), + + // Verify memory gateway entries have expected structure + const memoryEntries = Array.from( + ((gws.active.car.realGateway as DefSerdeGateway).gw as MemoryGateway).memories.entries(), + ) + .filter(([k]) => k.startsWith(`memory://${name}`)) + .map(([k]) => + stripper( + ["name", "storekey", "version", "key"], + Array.from(URI.from(k).getParams).reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}), ), - ).toEqual([ - { - key: "baembeig2is4vdgz4gyiadfh5uutxxeiuqtacnesnytrnilpwcu7q5m5tmu", - store: "car", - suffix: ".car", - }, - { - key: "main", - store: "wal", - }, - { - key: "main", - store: "meta", - }, - { - key: "baembeieldbalgnyxqp7rmj4cbrot75gweavqy3aw22km43zsfufrihfn7e", - store: "car", - suffix: ".car", - }, - ]); + ); + + // Should have car entries (with .car suffix), wal, and meta stores + const stores = memoryEntries.map((e) => e.store); + expect(stores.filter((s) => s === "car").length).toBe(2); + expect(stores).toContain("wal"); + expect(stores).toContain("meta"); const db1 = fireproof(name, { storeUrls: { @@ -160,10 +162,12 @@ describe("meta check", () => { }); expect(db1.ledger).not.equal(db.ledger); await db1.ready(); - expect(db1.ledger.crdt.blockstore.loader.carLog.asArray().map((i) => i.map((i) => i.toString()))).toEqual([ - ["baembeieldbalgnyxqp7rmj4cbrot75gweavqy3aw22km43zsfufrihfn7e"], - ["baembeig2is4vdgz4gyiadfh5uutxxeiuqtacnesnytrnilpwcu7q5m5tmu"], - ]); + + // Verify reopened database has same carLog structure + const carLog1 = db1.ledger.crdt.blockstore.loader.carLog.asArray(); + expect(carLog1.length).toBe(2); + expect(carLog1[0].length).toBeGreaterThan(0); + expect(carLog1[1].length).toBeGreaterThan(0); const gensis = await db1.get(PARAM.GENESIS_CID); expect(gensis).toEqual({ _id: PARAM.GENESIS_CID }); @@ -178,28 +182,19 @@ describe("meta check", () => { }, }, ]); - const car = Array.from(((gws.active.car.realGateway as DefSerdeGateway).gw as MemoryGateway).memories.entries()) + + // Verify car files exist in memory gateway (content is encrypted, so we can't decode directly) + const carEntries = Array.from( + ((gws.active.car.realGateway as DefSerdeGateway).gw as MemoryGateway).memories.entries(), + ) .filter(([k]) => k.startsWith(`memory://${name}`)) - .map(([k, v]) => [URI.from(k).getParam(PARAM.KEY), v]) - .find(([k]) => k === "baembeig2is4vdgz4gyiadfh5uutxxeiuqtacnesnytrnilpwcu7q5m5tmu") as [string, Uint8Array]; - const rawReader = await CarReader.fromBytes(car[1]); - const blocks = []; - for await (const block of rawReader.blocks()) { - blocks.push(block); - } - expect(dagCbor.decode(blocks[1].bytes)).toEqual({ - doc: { - _id: "baembeiarootfireproofgenesisblockaaaafireproofgenesisblocka", - }, - }); + .filter(([k]) => URI.from(k).getParam(PARAM.STORE) === "car"); - expect(blocks.map((i) => i.cid.toString())).toEqual([ - "bafyreibxibqhi6wh5klrje7ne4htffeqyyqfd6y7x2no6wnhid4nixizau", - "bafyreidnvv4mwvweup5w52ddre2sl4syhvczm6ejqsmuekajowdl2cf2q4", - "bafyreihh6nbfbhgkf5lz7hhsscjgiquw426rxzr3fprbgonekzmyvirrhe", - "bafyreiejg3twlaxr7gfvvhtxrhvwaydytdv4guidmtvaz5dskm6gp73ryi", - "bafyreiblui55o25dopc5faol3umsnuohb5carto7tot4kicnkfc37he4h4", - ]); + expect(carEntries.length).toBe(2); + // Verify each car entry has data (encrypted content) + carEntries.forEach(([, data]) => { + expect(data.length).toBeGreaterThan(0); + }); }); }); diff --git a/core/tests/fireproof/stable-cid.test.ts b/core/tests/fireproof/stable-cid.test.ts index 04d4cbea1..496e6d3a7 100644 --- a/core/tests/fireproof/stable-cid.test.ts +++ b/core/tests/fireproof/stable-cid.test.ts @@ -8,16 +8,17 @@ import { getKeyBag } from "@fireproof/core-keybag"; import { CryptoAction, IvKeyIdData } from "@fireproof/core-types-blockstore"; const sthis = ensureSuperThis(); + +/** + * Tests regression of stable CID encoding with encrypted storage. + * Note: insecure storekey has been removed (see spec-03). + */ describe.each([ async () => { const kb = await getKeyBag(sthis, {}); const keyStr = base58btc.encode(toCryptoRuntime().randomBytes(kb.rt.keyLength)); return await keyedCryptoFactory(URI.from(`test://bla?storekey=${keyStr}`), kb, sthis); }, - async () => { - const kb = await getKeyBag(sthis, {}); - return await keyedCryptoFactory(URI.from(`test://bla?storekey=insecure`), kb, sthis); - }, ])("regression of stable cid encoding", (factory) => { let kycr: CryptoAction; beforeEach(async () => {