diff --git a/apps/mana/apps/web/src/lib/components/EncryptionIntroBanner.svelte b/apps/mana/apps/web/src/lib/components/EncryptionIntroBanner.svelte new file mode 100644 index 000000000..e40a7a99b --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/EncryptionIntroBanner.svelte @@ -0,0 +1,215 @@ + + + +{#if visible} + +{/if} + + diff --git a/apps/mana/apps/web/src/lib/data/crypto/index.ts b/apps/mana/apps/web/src/lib/data/crypto/index.ts index 65768cd78..eebf4466e 100644 --- a/apps/mana/apps/web/src/lib/data/crypto/index.ts +++ b/apps/mana/apps/web/src/lib/data/crypto/index.ts @@ -52,3 +52,5 @@ export { type VaultUnlockState, createVaultClient, } from './vault-client'; + +export { getVaultClient } from './vault-instance'; diff --git a/apps/mana/apps/web/src/lib/data/crypto/vault-instance.ts b/apps/mana/apps/web/src/lib/data/crypto/vault-instance.ts new file mode 100644 index 000000000..df8b4e5eb --- /dev/null +++ b/apps/mana/apps/web/src/lib/data/crypto/vault-instance.ts @@ -0,0 +1,39 @@ +/** + * Lazy-singleton wrapper around createVaultClient. + * + * Module-level vault clients are awkward to share because they need + * the auth store + the auth URL at construction time, neither of + * which are available at module-load (the auth store is initialised + * inside +layout.svelte's onMount). This wrapper builds the client + * the first time `getVaultClient()` is called and reuses it for all + * subsequent callers — root layout, settings page, future settings + * sub-pages, debug tools. + * + * The MemoryKeyProvider lives inside the vault client and is set + * via setKeyProvider during construction. Phase 3 already wired the + * record-helpers to read from getActiveKey(), so once any caller + * builds the singleton the rest of the data layer can encrypt and + * decrypt without knowing about the vault client at all. + */ + +import { createVaultClient, type VaultClient } from './vault-client'; +import { authStore } from '$lib/stores/auth.svelte'; +import { getManaAuthUrl } from '$lib/api/config'; + +let _instance: VaultClient | null = null; + +export function getVaultClient(): VaultClient { + if (!_instance) { + _instance = createVaultClient({ + authUrl: getManaAuthUrl(), + getToken: () => authStore.getAccessToken(), + }); + } + return _instance; +} + +/** Test-only reset hook so each integration test starts with a fresh + * client. Not exported from the crypto barrel — internal to this file. */ +export function _resetVaultInstanceForTesting(): void { + _instance = null; +} diff --git a/apps/mana/apps/web/src/routes/(app)/settings/security/+page.svelte b/apps/mana/apps/web/src/routes/(app)/settings/security/+page.svelte new file mode 100644 index 000000000..86513e729 --- /dev/null +++ b/apps/mana/apps/web/src/routes/(app)/settings/security/+page.svelte @@ -0,0 +1,397 @@ + + + + + Sicherheit · Einstellungen · Mana + + +
+
+

Sicherheit

+

+ Verschlüsselung deiner Inhalte auf diesem Gerät. Sensitive Felder werden mit AES-GCM-256 + verschlüsselt, bevor sie in die lokale Datenbank geschrieben werden. +

+
+ + +
+
+

Status

+ {badge.label} +
+ + {#if vaultState.status === 'unlocked'} +

+ Dein persönlicher Schlüssel ist auf diesem Gerät geladen. {totalEncryptedFields} + Felder über {encryptedTables.length} Tabellen werden verschlüsselt gespeichert. +

+ {:else if vaultState.status === 'locked'} +

+ Dein Schlüssel ist nicht geladen. Verschlüsselte Inhalte können nicht gelesen werden, bis du + dich erneut anmeldest oder den Schlüssel manuell lädst. +

+ + {:else} +

+ Es gab ein Problem beim Laden deines Verschlüsselungsschlüssels. Bitte melde dich neu an + oder prüfe deine Internetverbindung. +

+ + {/if} +
+ + +
+
+

Verschlüsselte Felder

+ {totalEncryptedFields} Felder, {encryptedTables.length} Tabellen +
+

+ Welche Spalten in welchen Tabellen verschlüsselt am Gerät liegen. Strukturelle Metadaten (IDs, + Zeitstempel, Status-Flags) bleiben absichtlich im Klartext, damit Indizes, Sortierungen und + Sync weiter funktionieren. +

+ +
+ + +
+
+

Schlüssel rotieren

+
+

+ Vorsicht: Beim Rotieren wird ein neuer Schlüssel generiert. Daten, die mit + dem alten Schlüssel verschlüsselt wurden, sind danach nicht mehr lesbar — es sei denn, sie + wurden vorher entschlüsselt und mit dem neuen Schlüssel neu geschrieben. Mana führt diesen + Re-Encrypt-Schritt aktuell nicht automatisch durch. +

+

+ Wann verwenden? Wenn du den Verdacht hast, dass dein Gerät kompromittiert wurde, oder als + regelmäßige Sicherheitspraxis nach einem Login von einem unbekannten Ort. +

+ {#if !confirmRotate} + + {:else} +
+ + +
+ {/if} +
+ + +
+
+

Was Mana sehen kann

+
+

+ Mana speichert deinen Schlüssel verschlüsselt auf dem Server (mit einer separaten + Schlüssel-Verschlüsselungs-Schlüssel-Strategie), damit du dich von neuen Geräten anmelden + kannst. Das bedeutet: +

+ +
+
+ + diff --git a/apps/mana/apps/web/src/routes/+layout.svelte b/apps/mana/apps/web/src/routes/+layout.svelte index 44ee4174a..b6e385e06 100644 --- a/apps/mana/apps/web/src/routes/+layout.svelte +++ b/apps/mana/apps/web/src/routes/+layout.svelte @@ -8,9 +8,9 @@ import { setCurrentUserId } from '$lib/data/current-user'; import { migrateGuestDataToUser } from '$lib/data/guest-migration'; import { installDataLayerListeners } from '$lib/data/data-layer-listeners'; - import { createVaultClient, hasAnyEncryption } from '$lib/data/crypto'; - import { getManaAuthUrl } from '$lib/api/config'; + import { getVaultClient, hasAnyEncryption } from '$lib/data/crypto'; import SuggestionToast from '$lib/components/SuggestionToast.svelte'; + import EncryptionIntroBanner from '$lib/components/EncryptionIntroBanner.svelte'; import OfflineIndicator from '$lib/components/OfflineIndicator.svelte'; import PwaUpdatePrompt from '$lib/components/PwaUpdatePrompt.svelte'; @@ -21,13 +21,9 @@ // initialisation, which previously caused effect_update_depth_exceeded. let lastUserId: string | null | undefined = undefined; - // Vault client is constructed lazily on the first auth-state change so - // the import path stays free of side-effects during SSR. Reused across - // all subsequent unlock/lock calls. - const vaultClient = createVaultClient({ - authUrl: getManaAuthUrl(), - getToken: () => authStore.getAccessToken(), - }); + // Lazy singleton — constructed on first call, reused everywhere + // (root layout, settings/security page, future settings sub-pages). + const vaultClient = getVaultClient(); // Push the active user id into the data layer whenever auth state changes. // The Dexie creating-hook reads this to auto-stamp `userId` on every record, @@ -96,3 +92,4 @@ +