Files
Shade/.scaffold/logs/2026-08-13-gcm-nonce-reuse-fikset.md
2026-08-14 11:58:49 +02:00

42 lines
3.6 KiB
Markdown

---
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å.