From 79ce7059c1b0e7ccc1fde79afa0c497c392c7422 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 25 May 2026 14:32:14 +0200 Subject: [PATCH] User-facing docs: inline "how it works" + /personvern detail page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two layers of explanation, addressing the gap between "I just want to use it" and "I want to understand the crypto." Inline: - Signup page: extends the existing one-liner with a "Les hvordan det virker" link. Opens in a new tab so the user doesn't lose the form (and any invite token) mid-read. - Public landing: gains a second muted paragraph that names the three visibility levels in plain language and links to the docs. Detail page (/personvern): - New Svelte component Personvern.svelte with sections for the three visibility levels, E2E key model, password vs recovery code, what the server stores vs doesn't, sessions, and explicit limits ("what crypto does NOT protect against"). - Written for non-technical users; still names the primitives (Argon2id, XChaCha20-Poly1305) and gestures at SECURITY.md for anyone who wants the engineering depth. App.svelte routing: - New /personvern path handled in parsePath - applyRoute branch sets view='personvern' - leavePersonvern() routes back to /home or / depending on auth - Persistent footer link on every view so the docs are always one click away Norwegian Bokmål throughout. No new dependencies. Bundle 34.2 KB → 36.8 KB gzipped (mostly the markdown-ish prose content). --- frontend/src/App.svelte | 32 ++++- frontend/src/components/Home.svelte | 17 ++- frontend/src/components/Personvern.svelte | 154 ++++++++++++++++++++++ frontend/src/components/Signup.svelte | 1 + 4 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/Personvern.svelte diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 5e43b31..2baa443 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -13,6 +13,7 @@ import PublicList from './components/PublicList.svelte'; import Admin from './components/Admin.svelte'; import ActivityPermalink from './components/ActivityPermalink.svelte'; + import Personvern from './components/Personvern.svelte'; /** * URL contract: @@ -20,6 +21,7 @@ * /home — authenticated dashboard (private+semi+public) * /a/:id — permalink to a single activity (any visibility) * //list — opt-in public list for that user + * /personvern — privacy + how-it-works long-form page * * Anything else (signup, login, recovery, profile, feedback, admin) is an * in-app view state, not a URL. We update window.history on view changes @@ -32,7 +34,8 @@ | 'home' // "/home" — full authenticated dashboard | 'profile' | 'feedback' | 'admin' | 'public-list' // "//list" - | 'permalink'; // "/a/:id" + | 'permalink' // "/a/:id" + | 'personvern'; // "/personvern" let view: View = $state('loading'); let publicListUsername = $state(''); @@ -42,7 +45,7 @@ let selfRegistryEnabled = $state(true); interface Route { - view: 'public-home' | 'home' | 'public-list' | 'permalink' | 'invite'; + view: 'public-home' | 'home' | 'public-list' | 'permalink' | 'invite' | 'personvern'; payload?: string; } @@ -50,6 +53,7 @@ const path = window.location.pathname; if (path === '/' || path === '') return { view: 'public-home' }; if (path === '/home' || path === '/home/') return { view: 'home' }; + if (path === '/personvern' || path === '/personvern/') return { view: 'personvern' }; const userList = path.match(/^\/([a-z0-9_-]{2,31})\/list\/?$/); if (userList) return { view: 'public-list', payload: userList[1] }; const perma = path.match(/^\/a\/([A-Za-z0-9-]+)\/?$/); @@ -122,9 +126,23 @@ // the token prefilled. If they're already logged in we still show the // landing — they can't claim an invite on top of an existing account. view = session.user ? 'public-home' : 'signup'; + } else if (route.view === 'personvern') { + view = 'personvern'; } } + function goPersonvern() { + pushUrl('/personvern'); + view = 'personvern'; + } + + function leavePersonvern() { + // Send the visitor wherever they "would have been" — landing if logged out, + // dashboard if logged in. Either is more useful than staying on the doc page. + if (session.user) goHome(); + else goPublicHome(); + } + function onAuthed() { pushUrl('/home'); view = 'home'; @@ -210,7 +228,17 @@ {:else if view === 'admin'} + {:else if view === 'personvern'} + {:else} {/if} + + diff --git a/frontend/src/components/Home.svelte b/frontend/src/components/Home.svelte index 7ba644c..78acccc 100644 --- a/frontend/src/components/Home.svelte +++ b/frontend/src/components/Home.svelte @@ -121,11 +121,18 @@
{#if publicOnly} -

- Her kan du lage og dele lister over ting å gjøre om vinteren for å - holde vinterdepresjonen unna. Og selvfølgelig kan du også lage lister - for andre formål — noen liker jo ikke sommeren heller. -

+
+

+ Her kan du lage og dele lister over ting å gjøre om vinteren for å + holde vinterdepresjonen unna. Og selvfølgelig kan du også lage + lister for andre formål — noen liker jo ikke sommeren heller. +

+

+ Du velger selv om hver oppføring er privat (kryptert i nettleseren + din), anonym (synlig uten navn), eller offentlig. + Mer om personvern og hvordan det virker. +

+
{:else if session.user}

{#if session.user.display_name?.trim()} diff --git a/frontend/src/components/Personvern.svelte b/frontend/src/components/Personvern.svelte new file mode 100644 index 0000000..8b6dcb7 --- /dev/null +++ b/frontend/src/components/Personvern.svelte @@ -0,0 +1,154 @@ + + +

+ {#if onBack} +
+ +
+ {/if} + +

Personvern og hvordan det virker

+ +

+ Vinterliste lar deg lage og dele lister over ting å gjøre om vinteren — + eller hva enn du vil bruke det til. Du velger selv om hver oppføring er + privat, anonym eller offentlig. Her er hva det betyr i praksis, og hva + serveren faktisk ser av det du skriver. +

+ +

De tre synlighetsnivåene

+
    +
  • + Privat: oppføringen krypteres i nettleseren din før den + sendes. Verken vi som driver tjenesten, eller noen som måtte få tak i + databasen, kan lese innholdet. Bare du kan låse den opp — med passordet + ditt eller gjenopprettingskoden. +
  • +
  • + Anonym (halv-offentlig): innholdet er synlig for alle, + men navnet ditt vises ikke ved siden av. Vi lagrer en intern referanse + til hvem som eier oppføringen (slik at du kan redigere eller slette den), + men API-et returnerer aldri den referansen til andre brukere. +
  • +
  • + Offentlig: innholdet er synlig for alle og + kreditert med visningsnavnet ditt (eller brukernavnet, hvis du har + satt det). Setter du ingen av delene, vises ingen attribusjon. +
  • +
+ +

Ende-til-ende-kryptering, kort forklart

+

+ For private oppføringer skjer all kryptering i nettleseren din. Når du + oppretter en konto, lager nettleseren en tilfeldig nøkkel som vi kaller + DEK (Data Encryption Key). Denne nøkkelen pakkes inn — krypteres — på + to måter: +

+
    +
  • + Én pakke åpnes med passordet ditt: nettleseren utleder en + passordavledet nøkkel (KEK_pw) ved hjelp av Argon2id, + en moderne nøkkelutledningsfunksjon som er treg med vilje for å gjøre + brute-force-angrep upraktiske. +
  • +
  • + Én pakke åpnes med gjenopprettingskoden: en høyentropi-kode som vises + én gang ved registrering og som du må ta vare på selv. Vi lagrer den + aldri på serveren. +
  • +
+

+ Selve innholdet krypteres med XChaCha20-Poly1305, en + autentisert krypteringsalgoritme — som betyr at ciphertekst som er tuklet + med, ikke kan dekrypteres uten at det oppdages. +

+ +

Passordet ditt forlater aldri nettleseren

+

+ Når du logger inn, sender vi ikke passordet til serveren. + Nettleseren utleder en separat verifikator fra passordet (Argon2id med + et eget salt, distinkt fra det som brukes til å pakke DEK-en) og sender + bare denne. Serveren kjører Bun.password.verify mot en hash + vi har lagret. Om databasen lekker, må en angriper både brute-force-knekke + verifikatoren og så gjøre samme treghet om igjen for å utlede + nøkkelen som åpner pakka — to separate angrep, ikke ett. +

+ +

Å bytte passord eller bruke gjenopprettingskoden

+

+ Bytter du passord, blir innholdet i den private listen din aldri + kryptert på nytt. Nettleseren låser bare opp DEK-en med det gamle + passordet og pakker den inn igjen med det nye. Det går raskt og er + sikkert: selve dataene rører vi aldri. Gjenopprettingskoden virker etter + samme prinsipp; den åpner sin egen pakke og lar deg sette et nytt + passord uten å miste noe. +

+

+ Når noen fullfører gjenoppretting, sjekker serveren først at avsenderen + faktisk kan gjenopprettingskoden (via en egen verifikator). Det betyr at + noen som bare kjenner eposten din, ikke kan låse deg ute fra kontoen. +

+ +

Hva serveren ser, og ikke ser

+

Vi lagrer per bruker:

+
    +
  • Eposten din (brukt som innloggings-id).
  • +
  • + Salter og innpakkede nøkler — disse er ikke hemmelige i seg selv; uten + passordet eller gjenopprettingskoden låser de ingenting opp. +
  • +
  • Hash av autentiseringsverifikatoren (ikke passordet — verifikatoren).
  • +
  • Visningsnavn, brukernavn og rolleflagg, om du har satt dem.
  • +
+

For private oppføringer lagrer vi kun:

+
    +
  • Ciphertekst (ulesbar uten DEK-en).
  • +
  • En tilfeldig nonce per oppføring.
  • +
  • Tidspunkt for opprettelse og siste endring.
  • +
+

+ Vi lagrer aldri: råpassordet, gjenopprettingskoden, DEK-en i lesbar form, + eller klartekst-innholdet i private oppføringer. +

+ +

Sesjoner og innlogging

+

+ Innlogging gir deg en httpOnly-cookie med en tilfeldig sesjonsverdi som + lagres i databasen vår. Vi bruker bevisst ikke JWT — ingen + kompliserte tokens som vi ikke kan trekke tilbake. Logger du ut, eller + fullfører gjenoppretting, slettes sesjonene umiddelbart. +

+ +

Hva som ikke er beskyttet

+

+ Krypteringen beskytter innholdet i de private + oppføringene dine. Den beskytter ikke mot: +

+
    +
  • + At noen får tilgang til nettleseren din mens du er innlogget. Da har de + DEK-en i minnet og kan lese alt. Logg ut når du forlater enheten. +
  • +
  • + Trafikkanalyse — antallet og størrelsen på dine private oppføringer er + synlige for noen som overvåker forbindelsen, selv om innholdet ikke er + det. +
  • +
  • + Skadelige nettleserutvidelser eller sårbarheter i nettleseren. Ingen + ende-til-ende-app kan beskytte mot at klienten kompromitteres. +
  • +
+ +

Vil du grave dypere?

+

+ Den fulle tekniske beskrivelsen ligger i prosjektets SECURITY.md. + Den dekker eksakt hvilke biter som lagres hvor, hvilke Argon2id-parametre + vi bruker, og hvordan vi tenker rundt avveiningene. +

+
diff --git a/frontend/src/components/Signup.svelte b/frontend/src/components/Signup.svelte index ca08816..455533d 100644 --- a/frontend/src/components/Signup.svelte +++ b/frontend/src/components/Signup.svelte @@ -81,6 +81,7 @@

Vi krypterer alt du markerer som privat i nettleseren din. Serveren ser aldri passordet ditt eller innholdet i private oppføringer. + Les hvordan det virker.