feat(secrets): tokens og kodeord forlader ikke længere huset med en kopi

Gmails refresh-token lå i klartekst i databasen. Det er ikke filen på serveren,
der er problemet, men kopierne af den: sikkerhedskopier kan hentes ned gennem
fladen, så hver eneste kopi, der er taget indtil nu, bar et levende token.

Derfor ligger nøglen uden for databasen — i miljøet, eller i en fil ved siden af
dataene med tilstand 0600. VAPID-parret er præcedens for en hemmelighed,
instansen selv laver og gemmer i basen, men det mønster dur ikke her: en nøgle i
samme fil som det, den låser, er ingen lås. Backup er VACUUM INTO af selve
databasen, så filen følger ikke med.

Kun de navngivne felter forsegles — refresh_token, access_token, password,
client_secret. Resten af klatten er det, man læser, når en postkasse driller:
hvilken mappe, hvilken etiket, hvilken adresse. At kryptere det med ville ikke
købe noget og ville koste muligheden for at kigge.

Ingen migration, og ingen der skal forbinde forfra: en værdi uden mærket læses
som sig selv, og næste skrivning lægger den forseglet på plads. Kolonnen
konverterer sig selv efterhånden som den bruges, i stedet for i én omgang, der
skal have ret om hver eneste række på én gang. Der er en prøve for netop det.

Fladen siger det på siden, hvor man henter kopien — for den dag nøglen betyder
noget, er den dag, man gendanner et andet sted og undrer sig over, at postkassen
er tavs. Runen er på v4; en ny variabel uden et versionsspring er usynlig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kristian Wind
2026-08-18 21:18:48 +02:00
co-authored by Claude Opus 5
parent aabbbac583
commit a38a79e9b6
11 changed files with 530 additions and 3 deletions
@@ -267,6 +267,13 @@
<p class="hint">
{t('data.backupsAttachments')}
</p>
<!-- Said here rather than only in the documentation: this is the page where
somebody downloads the file, and the day the key matters is the day
they are restoring it somewhere else and wondering why the mailbox is
silent. -->
<p class="hint">
{t('data.backupsSecrets')}
</p>
</header>
<div class="row">