CI / OpenAPI is valid (push) Successful in 38s
CI / Go (push) Failing after 18m36s
CI / End-to-end (push) Failing after 18m54s
CI / Docker image builds (push) Successful in 7m27s
Documentation / build (push) Failing after 7m7s
Documentation / deploy (push) Skipped
Release / Build and publish image (push) Failing after 9m59s
Grunden er den samme som for postkassers kodeord og OAuth-tokens: en sikkerhedskopi kan hentes gennem fladen, og indtil nu bar hver eneste af dem hver eneste note i klartekst. Noter er dér, folk skriver kodeord og API-nøgler ned — det ved vi, fordi der ligger sådan nogle i den installation, det her blev bygget til. **Både titel og krop.** Titlen *er* første linje, så en forseglet krop ved siden af en læselig titel ville efterlade begyndelsen af hver eneste note i klartekst. **Og de to kopier gik med.** Det var derfor det ikke var gjort for længe siden: `notes_fts` indekserede kroppen, og den genererede `fold`-søjle bar en translittereret kopi af den. At forsegle kroppen og lade dem stå ville have været teater — en angriber med filen kunne læse hver note ud af indekset. Begge er væk, og tabellen er bygget om, fordi SQLite ikke kan fjerne en genereret søjle på plads. **Søgningen åbner teksten i Go.** Målt frem for gættet: tolv hundrede noter à 3,3 kB koster 33–43 ms pr. søgning, og fladen venter 250 ms efter sidste tastetryk, før den spørger — så det ligger inde i pausen. Tiden går ikke med at åbne konvolutterne; fire megabyte AES-GCM er få millisekunder. Den går med at folde og småskrive hver krop, og det er lineært: regn med omtrent 350 ms ved ti tusind noter. Dér er det den forkerte form, og svaret er da et nøglet indeks — hvis pris er, at præfiks- og delordssøgningen falder bort. bm25 forsvandt med indekset. Det, der er beholdt, er den halvdel, der betød mest: titlen vejer ti gange kroppen. Med et loft pr. ord, så en note, der nævner ordet fyrre gange, ikke slår en, der nævner alle ordene én gang — det er den fejl, en ren optælling laver, og den er let at lave og svær at få øje på. **En bagudfyldning ved opstart, ikke efterhånden.** Instansens indstillinger forsegles, når de skrives, og det er rigtigt for dem. For et arkiv på tolv hundrede importerede noter, som ingen redigerer igen, betyder "når de skrives" aldrig — og det er præcis dét arkiv, der er værd at forsegle. Idempotent, så den koster et gennemløb af id'er hver anden gang, og `updated_at` røres ikke: at lukke en konvolut om en note er ikke at skrive den, og et stempel med i aften ville kaste den rækkefølge væk, importen brugte en dag på at hente hjem. En fejl under bagudfyldningen stopper opstarten. Halvdelen af noterne lukket er ikke i stykker — `v1:`-præfikset gør, at begge slags kan læses — men en instans, der starter og lader som om den beskytter noget, den ikke gør, er værre end en, der siger fra. Hvad der stadig ligger åbent, står i SECURITY.md og på dokumentationssiden: opgavetitler, beskrivelser, kommentarer, og `note_links`, som røber hvilke projekter og opgaver en note peger på, men ikke hvad der står. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
3.3 KiB
SQL
76 lines
3.3 KiB
SQL
-- verdande:rebuild-tables
|
||
--
|
||
-- Notekroppe forsegles.
|
||
--
|
||
-- Grunden er den samme som for postkassers kodeord og OAuth-tokens: en
|
||
-- sikkerhedskopi kan hentes gennem fladen, og indtil nu bar hver eneste af dem
|
||
-- hver eneste note i klartekst. Noter er dér, folk skriver kodeord og API-nøgler
|
||
-- ned — det ved vi, fordi der ligger sådan nogle i denne installation.
|
||
--
|
||
-- # Hvorfor det ikke var gjort for længe siden
|
||
--
|
||
-- Kroppen var kopieret to steder mere, og begge kopier var klartekst:
|
||
--
|
||
-- * `fold` — en genereret søjle med title || ' ' || body, translittereret, så
|
||
-- "grøn" kan findes ved at skrive "gron".
|
||
-- * `notes_fts` — et FTS5-indeks over titel, krop og fold.
|
||
--
|
||
-- At forsegle kroppen og lade dem stå ville have været teater: en angriber med
|
||
-- filen kunne læse hver note ud af indekset. Så de skal begge væk, og søgningen
|
||
-- skal skrives om til at åbne teksten i Go.
|
||
--
|
||
-- # Hvad det koster, og hvornår formen er forkert
|
||
--
|
||
-- Søgningen læser nu hver note. Målt: tolv hundrede noter à 3,3 kB koster 33–43 ms
|
||
-- pr. søgning, og fladen venter 250 ms efter sidste tastetryk, før den spørger — så
|
||
-- det ligger inde i pausen. Tiden går ikke med at åbne konvolutterne, men med at
|
||
-- folde og småskrive hver krop; det er lineært, så regn med omtrent 350 ms ved ti
|
||
-- tusind noter. Dér er det den forkerte form, og svaret er da et *nøglet* indeks:
|
||
-- en HMAC pr. ord under den samme nøgle. Bemærk hvad det koster, når det bliver
|
||
-- aktuelt — et nøglet indeks kan slå et helt ord op og ikke andet, så præfiks- og
|
||
-- delordssøgningen, der findes i dag, ville falde bort.
|
||
--
|
||
-- # Hvad der stadig ligger i klartekst
|
||
--
|
||
-- Alt andet. Opgavetitler, beskrivelser, kommentarer — og `note_links`, som er
|
||
-- id'er udledt af kroppen, så en fil røber stadig *hvilke* projekter og opgaver en
|
||
-- note peger på, men ikke hvad der står. Det er skrevet i SECURITY.md, og det er
|
||
-- ikke løst her.
|
||
--
|
||
-- # Selve ombygningen
|
||
--
|
||
-- SQLite kan ikke fjerne en genereret søjle på plads, så tabellen bygges om.
|
||
-- Rækkerne kopieres med kroppen, som den er: forseglingen sker i Go, hvor nøglen
|
||
-- er, og en opstart efter denne migration går rækkerne igennem og lukker dem. Til
|
||
-- den tid kan en søjle indeholde begge dele, og `v1:`-præfikset findes netop for
|
||
-- at kunne læses uden at gætte.
|
||
CREATE TABLE notes_sealed (
|
||
id TEXT PRIMARY KEY,
|
||
project_id TEXT REFERENCES projects (id) ON DELETE CASCADE,
|
||
title TEXT NOT NULL DEFAULT '',
|
||
body TEXT NOT NULL DEFAULT '',
|
||
created_by TEXT REFERENCES users (id) ON DELETE SET NULL,
|
||
pinned INTEGER NOT NULL DEFAULT 0,
|
||
created_at INTEGER NOT NULL,
|
||
updated_at INTEGER NOT NULL,
|
||
deleted_at INTEGER,
|
||
archived_at INTEGER
|
||
);
|
||
|
||
INSERT INTO notes_sealed
|
||
(id, project_id, title, body, created_by, pinned, created_at, updated_at, deleted_at, archived_at)
|
||
SELECT id, project_id, title, body, created_by, pinned, created_at, updated_at, deleted_at, archived_at
|
||
FROM notes;
|
||
|
||
DROP TRIGGER notes_fts_insert;
|
||
DROP TRIGGER notes_fts_delete;
|
||
DROP TRIGGER notes_fts_update;
|
||
DROP TABLE notes_fts;
|
||
DROP TABLE notes;
|
||
|
||
ALTER TABLE notes_sealed RENAME TO notes;
|
||
|
||
CREATE INDEX notes_by_project ON notes (project_id, deleted_at, updated_at DESC);
|
||
CREATE INDEX notes_current ON notes (archived_at, deleted_at, updated_at DESC)
|
||
WHERE deleted_at IS NULL;
|