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.