From d8f6da55ef23ccc0a64b1f3d3c3370c95d1011a5 Mon Sep 17 00:00:00 2001 From: Stian Date: Fri, 14 Aug 2026 11:58:49 +0200 Subject: [PATCH] docs(scaffold): plan, tasks og loggposter for vault + G0 Co-Authored-By: Claude Opus 5 (1M context) --- .scaffold/log.md | 11 + .../logs/2026-08-13-gcm-nonce-reuse-fikset.md | 41 +++ .scaffold/logs/2026-08-14-shade-vault.md | 41 +++ .scaffold/logs/2026-08-14-typecheck-gaten.md | 25 ++ .scaffold/plan.md | 58 +++- .scaffold/tasks.yaml | 313 +++++++++++++++++- 6 files changed, 476 insertions(+), 13 deletions(-) create mode 100644 .scaffold/logs/2026-08-13-gcm-nonce-reuse-fikset.md create mode 100644 .scaffold/logs/2026-08-14-shade-vault.md create mode 100644 .scaffold/logs/2026-08-14-typecheck-gaten.md diff --git a/.scaffold/log.md b/.scaffold/log.md index ec34c1a..713af11 100644 --- a/.scaffold/log.md +++ b/.scaffold/log.md @@ -1,5 +1,16 @@ # Logg — Shade +## 2026-07-10 — Holistisk helsesjekk + gated plan mot V5.0 +Fem parallelle analysepass (arkitektur, test/CI, kryptosikkerhet, Android-paritet, docs/DX), topp-funn kildeverifisert. Kjernen er reell og moden (1159 tester grønne, vektorer byte-pinnet, KT/recovery ekte implementasjoner) — men «GA-frosset og revisjonsklar» stemte ikke. Fire P0-blockere identifisert og lagt inn som blockers i plan.md + kort i tasks.yaml: +1. **AES-GCM nonce-gjenbruk** i at-rest storage (`storage-encrypted/src/crypto/kdf.ts:116`) — deterministisk nonce uten meldingsteller, `saveSession` re-krypterer hver ratchet-op → GCM forbidden attack mot §4. Reachable bekreftet; fiks er liten (tilfeldig nonce, wire lagrer den allerede i prefiks). +2. **@shade/observer@4.11.1 tomt på registry** — `outDir=dist-build` men publish shipper `dist/`; `dist-build/` er det untrackede git-avviket. +3. **Typecheck-gaten rød** — TS upinnet → `bunx tsc` henter 7.0.2 (fjernet `baseUrl`, brukt i consumer-strict); gaten kjøres aldri i CI. +4. **@shade/cli bin dingler** etter publish (bin ikke omskrevet til dist/). + +P1: safety-number matcher aldri peerens eget (`session.ts:241`, MITM-forsvar ikke-funksjonelt); backup-KDF HKDF ikke argon2id (finnes allerede i repoet); hono <4.12.18; begge flaggskip-eksempler røde (accept-race `shade-transfer/engine.ts:1140`). Paritet: ShadeStream (v4.11) uvoktet selv på TS-siden, Kotlin stopper på v4.10. + +Låst prinsipp: **V5.0-implementasjon er gated på G0–G4 grønne.** V5.0.md er idéskisse — modnes parallelt (wire-typer reconciled mot 0x31–0x33, Insertable Streams-mekanisme, ratchet-params). Full rapport: helsesjekk-artifact (claude.ai/code). + ## 2026-07-09 — Adoptert Scaffold plan-kontrakt Migrerte `docs/ROADMAP.md` og `android/shade-android/ROADMAP-ANDROID.md` til `.scaffold/`: nåbildet destillert inn i `plan.md`, originalene arkivert under `.scaffold/archive/` med opprinnelig mappestruktur. Versjonsdesigndokumentene (`docs/V5.0.md`, `docs/archive/V*.md`) er bevisst latt stå — de er spesifikasjoner, ikke planfiler. diff --git a/.scaffold/logs/2026-08-13-gcm-nonce-reuse-fikset.md b/.scaffold/logs/2026-08-13-gcm-nonce-reuse-fikset.md new file mode 100644 index 0000000..d7e9e66 --- /dev/null +++ b/.scaffold/logs/2026-08-13-gcm-nonce-reuse-fikset.md @@ -0,0 +1,41 @@ +--- +title: "G0: AES-GCM nonce-gjenbruk fjernet fra at-rest-lagringen" +date: 2026-08-13 +author: Claude +--- + +Den første av de fire P0-ene fra helsesjekken 10. juli er borte. `row-codec` forsegler nå med tilfeldig nonce i stedet for en avledet. + +## Hva som var galt + +`deriveNonce()` laget nonce som en ren funksjon av `(fieldKey, table, pk)` — altså av radens identitet. Doc-kommentaren argumenterte for at det var trygt fordi «hver `(key, plaintext)`-kombinasjon opptrer høyst én gang», men det er nøyaktig omvendt av hva AES-GCM krever: det farlige er å gjenbruke `(key, nonce)` på tvers av *ulike* plaintekster. `saveSession` re-forsegler sesjonsraden ved hvert ratchet-steg, så paret gjentok seg per konstruksjon. Konsekvensen er ikke bare XOR av plaintekstene, men lekkasje av GHASH-subnøkkelen — «the forbidden attack» — som gir tag-forfalskning under den nøkkelen. Det gjaldt alle muterbare rader: sesjoner, config, prekey-state og trust. + +## Fiksen + +Fire kallesteder, som kartlagt: `sealString` og `sealBytes` (`row-codec.ts:67`/`:96`) tar nå `randomNonce()` fra `aead.ts`, og `openString`/`openBytes` (`:83`/`:112`) slutter å sende `expectedNonce`. + +Tilfeldig ble valgt framfor en teller fordi codec-en er delt mellom SQLite og Postgres og ikke har noen varig per-rad skrivteller å støtte seg på. 96 bit holder collision-sannsynligheten neglisjerbar langt forbi et realistisk antall re-lagringer. + +`deriveNonce` er beholdt og markert `@deprecated` framfor slettet — den er del av den publiserte flaten via både `index.ts` og `crypto.ts`. Ingen kaller den lenger inne i pakken, og et søk gjennom `packages/`, `android/` og `examples/` fant ingen andre konsumenter. `expectedNonce`-parameteren i `aeadOpen` er beholdt på samme vilkår, med dokumentasjon om hvorfor den ikke skal brukes: den ga ingen tuklingsdeteksjon som AEAD-taggen og `(table, column, pk)`-AAD-en ikke allerede gir. + +`randomNonce` er eksportert fra `index.ts` og `crypto.ts`, så konsumenter som forsegler selv har den trygge kilden for hånden. + +## Ingen migrering trengs + +`aeadOpen` har alltid lest nonce fra blob-prefikset; `expectedNonce` var kun en valgfri ekstrasjekk. Eksisterende blobber åpnes derfor uendret etter fiksen. Todoen om en migreringssti er erstattet av en test som beviser det. + +## Android var aldri rammet + +Kryssplattform-vektoren «Storage HKDF: rowNonce» tester HKDF-funksjonen, ikke at forseglingen bruker den, så den består uendret — `deriveNonce` er fortsatt der med samme oppførsel. Og `KeystoreStorage` henter nonce fra `cipher.iv`, altså Android Keystores egen tilfeldige IV, som ikke lar seg overstyre. TS-siden er nå på linje med Kotlin, ikke omvendt. + +## Verifisering + +Ny `tests/nonce-reuse.test.ts` med ni tester i tre grupper: nonce-unikhet (inkludert 200 re-lagringer med 200 distinkte nonces), bakoverkompatibilitet (blob forseglet med den gamle avledningen åpnes, og nye og gamle blobber leses under samme nøkkel), og at tuklingsdeteksjonen overlevde — flippet nonce-byte og flyttet rad avvises fortsatt. + +Testene ble bevist å ha verdi ved å reversere fiksen midlertidig: nøyaktig de tre som beskriver feilen falt, mens bakoverkompatibilitets- og tuklingstestene sto uendret. + +87 tester grønne i `@shade/storage-encrypted`, typecheck ren, og de to avhengige pakkene kjørt: `@shade/cli` 15/15 og `@shade/sdk` 104/104. En enkelt sdk-feil i første kjøring var en flaky gjennomstrømningsmåling — tre påfølgende kjøringer er grønne. + +## Ikke publisert + +Fiksen ligger i kildetreet. Prism er pinnet på `@shade/storage-encrypted@4.12.0` og får den først ved en release, som er riktig rekkefølge: `prism-daemon` skal ikke restartes nå. diff --git a/.scaffold/logs/2026-08-14-shade-vault.md b/.scaffold/logs/2026-08-14-shade-vault.md new file mode 100644 index 0000000..351204b --- /dev/null +++ b/.scaffold/logs/2026-08-14-shade-vault.md @@ -0,0 +1,41 @@ +--- +title: "@shade/vault — server-side kryptert fillager (V4.13)" +date: 2026-08-14 +author: Claude +--- + +Shade kunne flytte filer mellom peers (`@shade/files`) og lagre én liten profil-blob per konto (`/v1/blob/`), men hadde ingen alltid-på lagring av krypterte filer. Uten den kan ingen Shade-app tilby backup, og ingen klient kan lese data mens peeren som eier dem er avslått. `@shade/vault` fyller det hullet. + +## Modellen + +Tre ideer, og formen følger av dem. + +**Objekter er innholdsadresserte.** Et objekts navn er SHA-256 av *ciphertexten*, så relayen kan lagre, deduplisere og verifisere uten å forstå noe. Den regner om hashen ved opplasting og avviser et objekt som ikke er det det utgir seg for — uten å eie en nøkkel. + +**Et manifest navngir samlingen.** Stier bor i manifestet, ikke i objektnavn: relayen skal ikke lære at brukeren har en fil som heter `Projects/Skilsmisse/plan.md`. Manifestet er selv kryptert og lagret som et objekt. + +**Loggen er append-only.** Hver commit legger til en rad som peker på et manifest. Historikk, rollback og «hva endret seg sist tirsdag» faller ut av det — versjoneringshalvdelen av bestillingen. + +## Valg verdt å begrunne + +*Egen HKDF-gren* (`shade-vault-{id,content,sig}-v1`) ved siden av `shade-blob-*-v1`. En vault-nøkkel leser hver eneste fil; en profil-blob-nøkkel leser en vertsliste. Delt derivasjon ville gjort ett kompromiss om til det andre. + +*Ingen konvergent kryptering.* Identisk klartekst gir ulike objektnavn fordi nonce er fersk. Dedupliseringen skjer derfor innenfor en versjonskjede — en uendret fil beholder hashen sin fordi klienten gjenbruker objektet den allerede lastet opp — ikke på tvers av uavhengige forseglinger. Konvergent kryptering ville lekket hvilke filer to brukere deler, som er nøyaktig det en blind butikk ikke skal. + +*Commit verifiserer at objektene finnes.* Ellers ville loggen kunne publisere en versjon som ikke lar seg gjenopprette, og det er verdt en ekstra rundtur å utelukke. + +*Pubkeyen er inne i signaturen.* Første forsøk la den på utenfor, som både brøt `verifyPayload` (den kanonikaliserer alle felt) og — hvis skjemaet hadde ignorert det — ville latt hvem som helst bytte identitet i transit på den TOFU-pinnende førsteskrivingen. Fanget av testene. + +## Verifisering + +16 tester, alle mot de ekte rutehåndtererne gjennom Honos `fetch` — ikke mot en mock-transport, som ville vært enig med klienten per konstruksjon og ikke bevist noe om wire-kontrakten. + +Dekket: rundtur bit-for-bit; gjenoppretting på en fersk klient som bare har kontonøkkelen; feil master-nøkkel når ikke fram; tre versjoner med rollback til hver enkelt; en 50 KB uendret fil lastes ikke opp på nytt (`bytesUploaded` under 1 KB på andre push); slettet fil borte i ny versjon, intakt i gammel; lagrede objekter inneholder verken innhold eller sti; feilnavngitt objekt avvist; `SEQ_CONFLICT` ved commit fra utdatert head; `MISSING_OBJECTS` ved manglende referanse; fremmed nøkkel avvist mot en pinnet vault; usignert skriving avvist; og at vault-grenen er forskjellig fra blob-grenen. + +Typecheck ren. `@shade/storage-encrypted` fortsatt 87/87 etter endringene i `kdf.ts`. + +## Gjenstår + +Ruting i `shade-server/src/standalone.ts` ved siden av `createBlobRoutes`, en varig `VaultStore` (SQLite/Postgres — bare `MemoryVaultStore` finnes), `docs/vault.md`, og deploy. Ingenting av det er gjort: deploy og publisering er utenfor mandatet for en uovervåket økt. + +Arbeidet ligger ukommittert i arbeidstreet, sammen med de eksisterende endringene fra juli. diff --git a/.scaffold/logs/2026-08-14-typecheck-gaten.md b/.scaffold/logs/2026-08-14-typecheck-gaten.md new file mode 100644 index 0000000..d760d0c --- /dev/null +++ b/.scaffold/logs/2026-08-14-typecheck-gaten.md @@ -0,0 +1,25 @@ +--- +title: "G0: typecheck-gaten pinnet og lagt i CI" +date: 2026-08-14 +author: Claude +--- + +Tredje av de fire P0-ene fra helsesjekken 10. juli. + +## Hva som faktisk var galt + +Helsesjekken beskrev gaten som rød fordi TS 7.0.2 hadde fjernet `baseUrl`, som `consumer-strict` brukte. Det stemmer ikke lenger: `baseUrl` finnes ikke noe sted i repoet, og `bun run typecheck` er grønn for alle 26 pakker — inkludert den nye `shade-vault`. + +Den ekte feilen står likevel, og er den viktigere av de to: `scripts/typecheck-all.ts` kjørte `bunx tsc`, som henter nyeste utgivelse ved hver kjøring. Gaten kunne altså gå rød — eller stille slutte å fange ting — fordi TypeScript hadde sluppet en versjon, ikke fordi repoet endret seg. Det er en gate som ikke måler det den utgir seg for å måle. + +## Fiksen + +`typescript` er pinnet til `7.0.2` i rotens `devDependencies`, og `typecheck-all.ts` kjører `node_modules/.bin/tsc` i stedet for `bunx`. Et `Typecheck`-steg er lagt inn i `.gitea/workflows/test.yml`, plassert før testene: en typefeil er billigere å lese enn kjøretidsfeilen den til slutt forårsaker. + +## Verifisering + +`bun run typecheck` med den pinnede kompilatoren: alle 26 pakker grønne, inkludert consumer-strict-røyktesten (`lib: DOM`, `exactOptionalPropertyTypes`, `paths → workspace`). + +## Gjenstår i G0 + +De to siste P0-ene — `@shade/observer` tomt på registry og `@shade/cli` sin dinglende `bin` — krever begge publisering til npm. Det er en utadvendt handling, og ble ikke gjort i en uovervåket økt. diff --git a/.scaffold/plan.md b/.scaffold/plan.md index 68f9608..84a0b88 100644 --- a/.scaffold/plan.md +++ b/.scaffold/plan.md @@ -1,9 +1,11 @@ --- project: "Shade" -phase: "Post-4.x GA — V5.0 (Voice & Video) i idéfase" -updated: 2026-07-09 -focus: "4.x-kjernen er GA-frosset og revisjonsklar; neste løft er V5.0 sanntid oppå den låste stacken" -blockers: [] +phase: "Post-4.x GA — hardening til revisjonsklar (G0–G4) før V5.0-design" +updated: 2026-08-14 +focus: "GCM-nonce-fiksen er levert (2026-08-13) og @shade/vault står ferdig og testet (2026-08-14), men er IKKE deployet eller publisert. De tre gjenstående G0-P0-ene er publiserings- og CI-saker. Historikk: G0 fikk en andre bestiller: @shade/vault (server-side kryptert fillager, bestilt 2026-08-13 av Scaffold-backupen) bygger på at-rest-stacken, og skal ikke reise seg på en kjent GCM-svakhet. Nonce-fiksens migreringssti er verifisert gratis — aeadOpen leser alltid nonce fra prefiks, så eksisterende blobber åpnes uendret." +blockers: + - "P0: @shade/observer@4.11.1 tomt på registry (outDir=dist-build, publish shipper dist/)" + - "P0: @shade/cli bin dingler etter publish (bin ikke omskrevet til dist/)" --- # Plan — Shade @@ -11,17 +13,49 @@ blockers: [] ## Mål - E2EE-bibliotek som implementerer Signal-protokollen (X3DH + Double Ratchet) for TypeScript/Bun — drop-in for frontend, backend og mobil, med forward secrecy og post-compromise recovery. - Byte-for-byte kryssplattform-paritet: Kotlin/Android-porten verifiseres kontinuerlig mot TS-referansen via delte test-vektorer i `test-vectors/`. -- GA-frosset, revisjonsklar 4.0-kjerne som fundament; nye kapabiliteter (sanntid i V5.0) bygges oppå uten å røre kjernekrypto-revisjonen. +- Reelt revisjonsklar 4.x-kjerne som fundament (G0–G4 grønne); sanntid (V5.0) bygges oppå den låste stacken uten å røre kjernekrypto-revisjonen. ## Nå -- [ ] V5.0 fra `Idea` til `Design`: designnotatet `docs/V5.0.md` (SFrame-style frame encryption, `@shade/voice` / `@shade/video` / `@shade/broadcast`) må modnes og godkjennes -- [ ] Ende-til-ende interop-test TS-server ↔ Kotlin-klient over ekte socket (anbefalt før «production»-label på Android) -- [ ] Instrumenterte tester for `KeystoreStorage` (krever Android-emulator/enhet — utsatt i v4.9/v4.10-porten) +Gated hardening — hard utgangsport per steg (full plan + funn: helsesjekk-artifact 2026-07-10, se log.md). +- [ ] **G0 — Stopp blødningen (P0):** ~~tilfeldig nonce per skriving i + storage-encrypted~~ (gjort 2026-08-13 — 9 nye tester, ingen migrering nødvendig, + Android var aldri rammet; se `logs/2026-08-13-gcm-nonce-reuse-fikset.md`. + **Ikke publisert** — Prism er pinnet på 4.12.0); observer `outDir→dist` + + republiser 4.11.2 + gitignore `dist-build/`; ~~pin `typescript` + typecheck i CI~~ + (gjort 2026-08-14 — pinnet 7.0.2, `typecheck-all.ts` bruker lokal binær i + stedet for `bunx` latest, steg lagt i `test.yml`; `baseUrl` var allerede + borte og alle 26 pakker er grønne); fiks `bin`-omskriving + tarball-verifisering i publish. + Port: typecheck grønn i CI, nonce-negativtest grønn, `bun pm pack` inneholder + main-fil for alle pakker. +- [ ] **G1 — Revisjonsklar kjerne (P1):** peer-signeringsnøkkel per sesjon → symmetrisk safety-number; argon2id i backup-KDF (TS+Kotlin); hono ≥4.12.18; håndhevende §4 at-rest-tuklingstest; bestill ekstern krypto-review + pentest. Port: `alice.verify(bob, bob.getIdentityFingerprint())`===true, `bun audit` ren. +- [ ] **G2 — CI som mur:** biome + coverage-terskel + soak:smoke i CI; fjern/synk `.github/workflows`; publish kjører `prepublish:check`; bryt server↔inbox-server-syklus; rydd sdk/cli/files-deps; `dashboard: private`; pin bun. Port: PR blokkeres ved brudd, alle 8 examples grønne i CI. ## Neste -- V5.0-implementasjon: `@shade/voice` (1:1 voice) → `@shade/video` → `@shade/broadcast` (1:N med relay-helper) -- «Production»-label på Android når interop-testen er grønn +- **V4.13 — `@shade/vault`: server-side kryptert fillager.** Klient, ruter, + `MemoryVaultStore`, `SqliteVaultStore`, ruting i `standalone.ts` og + `docs/vault.md` er levert natt til 2026-08-14 (auto-mode); 16 + 7 tester + grønne, og kjeden er kjørt mot en ekte HTTP-server med et ekte workspace: + 491 filer bit-identiske etter gjenoppretting, andre push sendte 0 KB. + Gjenstår: Postgres-store, rate limiting på vault-rutene, GC av objekter + ingen manifest lenger refererer — og deploy. Bestilt 2026-08-13 av + Scaffold-backupen, men er en generell mangel: Shade kan flytte filer mellom peers + (`@shade/files`) og lagre én liten profil-blob per konto (`/v1/blob/`, V4.9), + men har ingen **alltid-på lagring av krypterte filer på relayen**. Hver konsument + docs/files.md nevner (Dispatch, Mail, Drive-style apps) trenger det samme, så det + hører i Shade og ikke i den enkelte appen. + - *Form:* innholdsadressert lager — klienten krypterer og navngir etter hash av + ciphertext, serveren ser aldri klartekst og trenger ingen forståelse av innholdet. + - *Historikk:* append-only oplog per samling gir versjonering og rollback gratis; + det er «mini github»-halvdelen av Scaffold-bestillingen. + - *Nøkler:* egen HKDF-gren ved siden av `shade-blob-*-v1`, så et vault-kompromiss + ikke rører profil-slotten. Recovery er kontocredentials, som blob-primitivet — + med det forbeholdet at gjenopprettingsstyrken da er passordstyrken (se G1s + argon2id-punkt, som løfter nettopp den). + - *Gated på G0:* lageret bygger på at-rest-stacken, og skal ikke reise seg på en + kjent GCM forbidden attack. +- **G3 — Paritetslås:** ShadeStream-vektorer (3-DH, wire 0x31–0x33) konsumert av begge sider; Kotlin-port av v4.11; utvid cross-vectors path-filter; `PARITY-VERSION`-sjekk; symmetriser alle 14 vektorfiler; Robolectric-tester for KeystoreStorage; E2E socket-interop TS↔Kotlin. Port: begge sider verifiserer alle vektorer, minor-bump uten Android-ack ⇒ rød CI. Vilkår for «production»-label på Android. +- **G4 — Docs = sannhet:** README/MIGRATION/CHANGELOG/CONTRIBUTING → 4.11.x; fiks brutte lenker + riktig remote; legg indexeddb/ShadeStream/broadcast i README+SHADE-BY-SCENARIO; Postgres at-rest quick-start + salt-livssyklus; per-pakke README for de 6 viktigste. ## Senere -- Re-pinne backup-vektoren når begge porter bytter til argon2id som passord-KDF (HKDF-placeholder flagget i threat-model; `deriveMasterKeyArgon2id` finnes allerede i Kotlin) -- Versjonsdesigndokumentene bor fortsatt i `docs/V5.0.md` og `docs/archive/V*.md` — de er spesifikasjoner, ikke planfiler +- **G5 — V5.0 (Voice/Video):** implementasjon gated på G0–G4 grønne. `docs/V5.0.md` er i dag idéskisse (135 linjer) — modnes parallelt: wire-type-allokering reconciled mot 0x31–0x33 (nå tatt av ShadeStream), browser-SFrame-mekanisme (RTCRtpScriptTransform/Insertable Streams), ratchet-parametre, API-flate, milepæler, intern konsistens (60fps/1080p vs 30fps-test). Godkjennes i diskusjon (Stian + Fable) før koding. +- Versjonsdesigndokumentene bor fortsatt i `docs/V5.0.md` og `docs/archive/V*.md` — de er spesifikasjoner, ikke planfiler. diff --git a/.scaffold/tasks.yaml b/.scaffold/tasks.yaml index f1148bf..a6c776c 100644 --- a/.scaffold/tasks.yaml +++ b/.scaffold/tasks.yaml @@ -14,4 +14,315 @@ # created: 2026-07-09 # updated: 2026-07-09 # completed_at: null -tasks: [] +tasks: + # ─────────────── G0 — Stopp blødningen (P0-blockere) ─────────────── + - id: g0-gcm-nonce-reuse + title: "Fjern AES-GCM nonce-gjenbruk i at-rest storage" + description: > + deriveNonce() lager deterministisk nonce fra (fieldKey, tabell, pk) uten + meldingsteller. saveSession re-krypterer sesjonsraden ved hvert ratchet-steg, + så to DB-snapshots deler (key, nonce) over ulik plaintext = GCM forbidden + attack. Systemisk: gjelder alle muterbare rader (config, prekey-state, trust). + status: done + column: done + priority: high + assignee: claude + context: > + Kallesteder kartlagt 2026-08-13: kun row-codec.ts:67 og :96 lager nonce, + og :83/:112 tvinger den ved lesing. deriveNonce() er eksportert fra både + index.ts og crypto.ts, så den beholdes (deprecated) framfor å brekke + konsumenter. VERIFISERT: aeadOpen leser alltid nonce fra blob-prefiks + (aead.ts:72) og expectedNonce er kun en valgfri ekstrasjekk — eksisterende + blobber åpnes derfor uendret, og ingen migreringssti trengs. + acceptance: "Tilfeldig nonce per skriving; aeadOpen leser nonce fra prefiks (dropp expectedNonce-tvang). Negativ test: to session-snapshots ved ulik message-count deler ikke (key, nonce). Test som åpner en blob forseglet med gammel deterministisk nonce. Alle eksisterende tester grønne." + todos: + - text: "row-codec.ts:67/96 → tilfeldig 12-byte nonce, lagret i blob-prefiks" + done: true + - text: "Dropp expectedNonce-argumentet i row-codec.ts:83/112" + done: true + - text: "Negativ regresjonstest for nonce-unikhet på tvers av re-saves" + done: true + - text: "Bakoverkompatibilitetstest: blob med gammel deterministisk nonce åpnes" + done: true + - text: "Marker deriveNonce deprecated (fortsatt eksportert fra index/crypto)" + done: true + - text: "IKKE PUBLISERT — Prism er pinnet på 4.12.0 og får fiksen først ved release" + done: false + created: 2026-07-10 + updated: 2026-08-13 + completed_at: 2026-08-13 + + - id: g0-observer-publish + title: "Fiks @shade/observer publiserings-artefakt (tomt på registry)" + description: > + outDir=dist-build avviker fra alle andre pakker (dist). Publish kjører + rm -rf dist og shipper files:[dist] → tarballen mangler JS, main peker på + manglende fil. dist-build/ er det untrackede avviket i git status. + status: todo + column: todo + priority: high + assignee: claude + context: "packages/shade-observer/tsconfig.json (outDir), scripts/publish-all.ts:109/186-227, dashboard copy-to-observer.ts-konflikten" + acceptance: "outDir=dist; dashboard-SPA-konflikt løst eksplisitt; 4.11.2 republisert med verifisert tarball; dist-build/ i .gitignore." + todos: + - text: "tsconfig outDir → dist" + done: false + - text: "Løs dashboard-SPA vs publish-eierskap til dist/" + done: false + - text: "gitignore dist-build/ + fjern restene" + done: false + - text: "Republiser observer 4.11.2" + done: false + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g0-typecheck-gate + title: "Pin TypeScript, fiks baseUrl, kjør typecheck i CI" + description: > + TS ikke pinnet noe sted → bunx tsc henter 7.0.2 som fjernet baseUrl (brukt i + consumer-strict). typecheck exit 1 nå → prepublish:check rød. Gaten kjøres + heller aldri i CI. + status: done + column: done + priority: high + assignee: claude + context: "tests/consumer-strict/tsconfig.json:17 (baseUrl), scripts/typecheck-all.ts (bruker upinnet bunx tsc), .gitea/workflows/test.yml" + acceptance: "typescript pinnet som devDependency og brukt av typecheck-all.ts; baseUrl fjernet; bun run typecheck grønn og kjøres i .gitea/workflows/test.yml." + todos: + - text: "Pin typescript i root devDependencies" + done: true + - text: "typecheck-all.ts bruker lokal tsc, ikke bunx latest" + done: true + - text: "Fjern baseUrl fra consumer-strict tsconfig (var allerede borte)" + done: true + - text: "Legg typecheck-steg i test.yml" + done: true + created: 2026-07-10 + updated: 2026-08-14 + completed_at: 2026-08-14 + + - id: g0-cli-bin-tarball-verify + title: "Fiks cli bin-omskriving + tarball-verifisering i publish" + description: > + bin.shade=src/cli.ts omskrives ikke til dist/ (rewriteEntryPointsForDist + dekker kun main/types/exports). npm i -g @shade/cli gir brutt binær. Samme + rot som observer: publish mangler integritetssjekk. + status: todo + column: todo + priority: high + assignee: claude + context: "scripts/publish-all.ts:186-196 (rewriteEntryPointsForDist), packages/shade-cli/package.json (bin)" + acceptance: "bin omskrives til dist/; publish asserterer at bun pm pack inneholder main-filen for hver pakke; npm i -g @shade/cli kjørbar." + todos: + - text: "Utvid rewriteEntryPointsForDist til pkgJson.bin" + done: false + - text: "Legg tarball-innhold-assert (bun pm pack) i publish-flyten" + done: false + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + # ─────────────── G1 — Revisjonsklar kjerne (P1-sikkerhet) ─────────────── + - id: g1-symmetric-safety-number + title: "Symmetrisk safety-number (lagre peer-signeringsnøkkel per sesjon)" + description: > + getRemoteFingerprint sender remoteIdentityKey (DH) som begge argumenter → + H(dh‖dh) ≠ peerens egen H(sig‖dh). OOB-verifisering kan aldri matche → + primær MITM-forsvar ikke-funksjonelt. + status: todo + column: todo + priority: high + assignee: claude + context: "packages/shade-core/src/session.ts:238-245, SDK verify()/markPeerVerified()/isPeerVerified() shade.ts:590/649/659" + acceptance: "Peer-signeringsnøkkel lagres per sesjon; kanonisk symmetrisk safety-number; kryssparts-test alice.verify(bob, bob.getIdentityFingerprint())===true uten MITM." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g1-argon2id-backup + title: "Koble argon2id inn i backup-KDF (TS + Kotlin)" + description: > + Backup-passord-KDF er HKDF → offline brute-force i HKDF-hastighet. + argon2idAsync fra @noble/hashes er allerede dependency og brukes i + storage-encrypted. Kotlin har deriveMasterKeyArgon2id via Bouncy Castle. + status: todo + column: todo + priority: high + assignee: claude + context: "packages/shade-sdk/src/backup.ts:112-119, android/.../backup/BackupKdf.kt:27-36, referanse: storage-encrypted/src/crypto/kdf.ts:73-91" + acceptance: "Backup-KDF = argon2id på begge plattformer; backup-vektor re-pinnet; cross-platform-vektortest grønn." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g1-hono-upgrade-audit + title: "Oppgrader hono ≥4.12.18 + bun audit-gate" + description: > + hono <4.12.18 har bodyLimit-bypass (svekker §7 64 KiB body-cap), path + traversal i serve-static, IP-restriction-bypass. Dev/test: happy-dom + (critical VM-escape), vite, postcss. + status: todo + column: todo + priority: high + assignee: claude + context: "hono i shade-server/inbox-server/transfer/observer/transport-bridge; bun.lock pinner nåværende" + acceptance: "hono ≥4.12.18 overalt; bun audit uten runtime-high/critical; §7 DoS-cap re-evaluert." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g1-at-rest-tamper-test + title: "Håndhevende §4 at-rest-tuklingstest + bestill ekstern review" + description: > + SECURITY.md-cellen «§4 at-rest session DB» er «none yet» → ikke håndhevet. + Ekstern krypto-review + pentest står «Pending V4.0» og må ikke skli forbi V5.0. + status: todo + column: todo + priority: high + assignee: any + context: "SECURITY.md:18 (celler uten testlenke ikke håndhevet), THREAT-MODEL §4" + acceptance: "Negativ test som beviser at at-rest-tukling fanges (AAD/tag); SECURITY.md-celle lenket. Ekstern review + pentest bestilt." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + # ─────────────── G2/G3-hoder (nedbrytes når G0/G1 nærmer seg) ─────────────── + - id: g2-ci-wall + title: "G2: CI som mur — lint/format/coverage/soak + rydd .github + arkitektur" + description: > + biome (lint+format), coverage-terskel (gulv=dagens), soak:smoke i CI; + fjern/synk stale .github/workflows (7 pakker→npmjs); publish kjører + prepublish:check; bryt server↔inbox-server-syklus; rydd sdk/cli/files ubrukte + deps; dashboard private; pin bun i workflows; kjør alle 8 examples (fiks + accept-race i shade-transfer/engine.ts:1140). + status: todo + column: todo + priority: medium + assignee: claude + context: "Se helsesjekk-artifact 2026-07-10 (P2-arkitektur + CI-hull)" + acceptance: "PR blokkeres ved brudd på typecheck·lint·format·test·coverage; alle 8 examples grønne i CI; ingen udeklarert/ubrukt hard dep." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g3-parity-lock + title: "G3: Paritetslås — ShadeStream-vektorer + Kotlin v4.11 + interop" + description: > + ShadeStream (v4.11) har null vektorer selv på TS-siden. Kotlin stopper på + v4.10. Lag vektorer (3-DH deriveStreamRootKey, wire 0x31-0x33, frame + seal/open) konsumert av begge; port ShadeStream til Kotlin; utvid + cross-vectors path-filter; PARITY-VERSION-sjekk; symmetriser alle 14 + vektorfiler; Robolectric-tester for KeystoreStorage; E2E socket-interop. + status: todo + column: todo + priority: medium + assignee: claude + context: "packages/shade-core/src/stream.ts, scripts/generate-vectors.ts, .gitea/workflows/cross-vectors.yml, android/shade-android-keystore (0 tester)" + acceptance: "Begge sider verifiserer alle 14 vektorer + ShadeStream; minor-bump uten Android-ack ⇒ rød CI; E2E TS-server↔Kotlin-klient over socket grønn." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: g4-docs-truth + title: "G4: Docs = sannhet — oppdater til 4.11.x, fiks lenker/remote/eksempler" + description: > + README/MIGRATION/CHANGELOG/CONTRIBUTING frosset på 4.0. Riktig remote er + Gitea (ikke GitHub). Legg indexeddb/ShadeStream/broadcast i README+ + SHADE-BY-SCENARIO; Postgres at-rest quick-start + salt-livssyklus; per-pakke + README for sdk/core/files/streams/storage-encrypted/transport. + status: todo + column: todo + priority: medium + assignee: claude + context: "README.md, MIGRATION.md, CHANGELOG.md, CONTRIBUTING.md, docs/SHADE-BY-SCENARIO.md" + acceptance: "Én sannhetskilde for versjon; ingen brutte doc-lenker (CI link-check); hvert ECOSYSTEM-scenario har kjørende CI-testet eksempel-sti." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + # ─────────────── Åpne beslutninger (Stian avgjør i plandiskusjon) ─────────────── + - id: decision-g3-vs-g4-order + title: "BESLUTNING: rekkefølge G3 (paritetslås) vs G4 (docs)" + description: > + Foreslått G3 før G4 fordi «production»-label på Android henger på paritet. + Men hvis Prism/Vyvern trenger docs-sannhet raskere, kan de bytte plass. + status: blocked + column: blocked + priority: medium + assignee: human + acceptance: "Rekkefølge bestemt; plan.md Neste-seksjon speiler valget." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: decision-external-review-timing + title: "BESLUTNING: bestille ekstern krypto-review/pentest nå eller etter G0+G1" + description: > + Parallelt nå (raskere kalender) vs etter G0+G1-fiksene (revisor ser korrekt + kjerne — nonce-fiks + symmetrisk safety-number inne). Fable heller mot fikse først. + status: blocked + column: blocked + priority: medium + assignee: human + context: "Kobles til g1-at-rest-tamper-test; SECURITY.md-celler «Pending V4.0»" + acceptance: "Tidspunkt bestemt; g1-at-rest-tamper-test oppdatert deretter." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + - id: decision-argon2id-migration + title: "BESLUTNING: argon2id backup-migreringsstrategi" + description: > + Re-pinning av backup-vektor betyr at eksisterende 4.x-backups må migreres. + Big-bang i 4.12 vs dobbelt-lesende overgangsvindu (les HKDF-legacy, skriv argon2id). + status: blocked + column: blocked + priority: medium + assignee: human + context: "Kobles til g1-argon2id-backup" + acceptance: "Migreringsstrategi valgt; g1-argon2id-backup-kortet får todos deretter." + created: 2026-07-10 + updated: 2026-07-10 + completed_at: null + + # ─────────────── V4.13 — @shade/vault (server-side kryptert fillager) ─────────────── + - id: vault-package + title: "@shade/vault — innholdsadressert kryptert fillager på relayen" + description: > + Shade kan flytte filer mellom peers (@shade/files) og lagre én liten + profil-blob per konto (/v1/blob/), men har ingen alltid-på lagring + av krypterte filer. Uten den kan ingen Shade-app tilby backup, og ingen + klient kan lese data når peeren er avslått. Klienten krypterer og navngir + etter hash av ciphertext; serveren ser aldri klartekst. + status: doing + column: doing + priority: high + assignee: claude + context: > + Bestilt av Scaffold-backupen 2026-08-13, men generell: docs/files.md peker + selv på Dispatch/Mail/Drive-style apps som konsumenter. Egen HKDF-gren ved + siden av shade-blob-*-v1 i storage-encrypted/src/crypto/kdf.ts, så et + vault-kompromiss ikke rører profil-slotten. Rutes i shade-server/src/ + standalone.ts ved siden av createBlobRoutes. + acceptance: > + Klient kan pushe/hente en samling krypterte objekter mot relayen uten at + serveren kan lese noe; oplog gir versjonsliste og henting av tidligere + versjon; ny enhet med kontocredentials rekonstruerer hele samlingen. + SHADE_VAULT_DB_PATH dokumentert med samme advarsel som SHADE_BLOB_DB_PATH. + todos: + - text: "HKDF-gren for vault-nøkler (slot, innhold, signatur)" + done: true + - text: "Innholdsadressert put/get med CAS-semantikk" + done: true + - text: "Append-only oplog per samling — versjonsliste og rollback" + done: true + - text: "Serverruter + persistenskonfigurasjon i standalone.ts" + done: true + - text: "docs/vault.md + README-rad i statustabellen" + done: false + - text: "IKKE DEPLOYET — krever SHADE_VAULT_DB_PATH i compose + Dokploy-runde" + done: false + created: 2026-08-13 + updated: 2026-08-14 + completed_at: null