Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ Non-obvious decisions not visible from code alone:
- **Auth store `isRestoringSession`**: defaults `true`; `reset()` does NOT touch it (logout doesn't re-trigger initialization)
- **Auto-lock**: `useVaultTimeout` hook in ProtectedLayout resets a 15-minute inactivity timer on user activity (mousemove, keydown, mousedown, touchstart, scroll); calls `lockVault()` on expiry
- **VaultUnlockDialog**: uses a separate `vault-dialog-store` so the dialog can be opened/closed independently of vault lock state. This lets the user dismiss the dialog without unlocking, and lets the sidebar/mobile nav trigger `openUnlockDialog()` directly
- **`keyVault.lockVault()` vs `keyVault.clearVault()`**: `lockVault()` preserves the cached envelope (so re-unlock can skip network calls), while `clearVault()` (called on logout) zeros everything including the cache. `unlockVault()` (in `auth-flow.ts`) tries the cached envelope first and retries from server on `DecryptionError` (stale cache can happen if the password was changed in another session)
- **`keyVault.lockVault()` vs `keyVault.clearVault()`**: `lockVault()` preserves the cached envelope (so re-unlock can skip network calls), while `clearVault()` (called on logout) zeros everything including the cache. `keyVault.unlockVault()` tries the cached envelope first and retries from server on `DecryptionError` (stale cache can happen if the password was changed in another session)
- **Test file naming**: prefix with `-` in `src/app/routes/` to exclude from TanStack Router route tree generation
- **Test setup**: shared setup (`src/test/setup.ts`) resets `useAuthStore` (with `isRestoringSession: false`), `useCryptoStore`, and `useUiStore` (including `sidebarWidth: 240`) in `afterEach`. Router mocking (`@tanstack/react-router`) is done per-file in each test that needs it, not centralized
- **Argon2id Web Worker**: `argon2id.ts` delegates all derivation to `argon2id.worker.ts` via `postMessage`. The worker lazy-loads `argon2-browser/dist/argon2-bundled.min.js` (not the default `argon2-browser` import — the default tries to load a `.wasm` file which Vite cannot handle; the bundled build embeds WASM as base64 in JS). Tests mock the Worker constructor; actual Argon2id computation is tested in E2E (Step 36).
Expand All @@ -195,7 +195,7 @@ Non-obvious decisions not visible from code alone:
- **BIP-39 mnemonic functions are async**: `generateMnemonic`, `validateMnemonic`, `mnemonicToSeed` must be `async` despite the underlying `@scure/bip39` functions being synchronous, because the lazy-loading pattern (`await loadBip39()`) requires it. Same as how `argon2id.ts` wraps sync Argon2 in async.
- **`deriveRecoveryKEK` uses mnemonic string directly**: The mnemonic phrase is passed as the Argon2id "password" parameter, not the BIP-39 binary seed. The human-readable phrase is the input because it is what the user supplies and remembers; the binary seed is an internal derivation artifact. `mnemonicToSeed` is a utility function not used in the recovery KEK path.
- **Crypto integration tests mock `deriveKey` re-consumption**: In `crypto-integration.test.ts`, `unwrapMasterKeyWithRecovery` requires a fresh `deriveKey` mock even after `wrapMasterKeyWithRecovery` consumed one during setup. The `setupRegistration` helper uses `mockResolvedValueOnce` which is consumed, so the test must re-mock before calling unwrap.
- **`deriveRegistrationKeys` is a pure crypto function**: in `features/encryption/model/registration.ts`, it has no side effects (no auth, no DB, no store writes). The orchestration (signup + upload + store population) lives in `auth-flow.ts` `signUpUser`. Do not add side effects to this function.
- **`deriveRegistrationKeys` is a pure crypto function**: in `features/auth/model/registration-crypto.ts`, it has no side effects (no auth, no DB, no store writes). The orchestration (signup + upload + store population) lives in `src/features/auth/model/auth-service.ts` `signUpUser`. Do not add side effects to this function.
- **`signUpUser` error cleanup**: on any error after `deriveRegistrationKeys` succeeds, attempts `authAdapter.logout()` as best-effort cleanup (harmless if no session exists, since Supabase signOut with no session is a no-op).
- **Login salt fetch is pre-auth**: `get_login_salts(p_username)` is a SECURITY DEFINER RPC callable by anonymous users, rate-limited (5 req/2 min/IP). Salts must be fetched before auth to derive `authHash` for Supabase Auth, but the `keys` table is RLS-protected. After auth succeeds, `fetchMasterKeyEnvelope` and `fetchFieldKeys` fetch wrapped key material through standard RLS-protected queries.
- **Auth error codes fold username format into invalid credentials**: `AuthErrorCode.INVALID_USERNAME_FORMAT` doesn't exist — `supabase-keys.ts` throws `INVALID_CREDENTIALS` for invalid username format. This is deliberate: showing a different error for "wrong format" vs "wrong password" would leak whether a username exists. Missing key data is `ApiError(NOT_FOUND)`, not an auth error — `KEYS_NOT_FOUND` was removed from `AuthErrorCode` because "data not found" is a data-layer concern, not an auth concern.
Expand Down
75 changes: 45 additions & 30 deletions docs/implementation-plan/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,6 @@ cipher-note-react/
src/
app/
Providers.tsx # QueryClientProvider, i18n, AuthProvider
flows/
auth-flow.ts # Orchestrate: signup, login, logout, session restore, unlock vault
router.tsx # TanStack Router route tree
ErrorBoundary.tsx # Root error boundary with crypto error handling
styles/
Expand Down Expand Up @@ -95,38 +93,49 @@ cipher-note-react/
ui/
AuthLayout.tsx # Shared layout for auth pages
AuthProvider.tsx # Wires auth store into AuthContext (depends on features, so not in shared/)
LoginPage.tsx # Login page with password input
RegisterPage.tsx # Registration page with mnemonic display
MnemonicDialog.tsx # Seed phrase display
MnemonicInput.tsx # 12-word input with BIP-39 validation
PasswordStrength.tsx # Password strength indicator
UsernameAvailability.tsx # Real-time username availability check
lib/
RequireAuth.tsx # Redirect to /login if not authenticated
GuestOnly.tsx # Redirect to /dashboard if authenticated
fields/
model/
field-crypto.ts # encrypt/decrypt field content
field-service.ts # load/save fields via API
auto-save.ts # Debounced auto-save with optimistic updates
sync-status.ts # Zustand: per-field sync status
use-field-editor.ts # Local draft + debounce logic
use-save-scheduler.ts # Debounced save with refs
use-field-query.ts # TanStack Query hook for field content
use-field-mutation.ts # TanStack Query mutation for saves
sync-status-store.ts # Zustand: per-field sync status
ui/
FieldCard.tsx # Locked/unlocked field display
NoteField.tsx # Textarea for note content
WebsiteField.tsx # Input for website URL
EmailField.tsx # Input for email address
SaveIndicator.tsx # "Saving..." / "Saved" / "Error"
ConflictNotification.tsx
DashboardPage.tsx # Main dashboard with all three fields
encryption/
model/
vault-timeout.ts # Auto-lock after inactivity
key-rotation.ts # Rotate individual field keys
multi-device.ts # Handle key changes from other sessions
registration.ts # Pure crypto: deriveRegistrationKeys
encryption-facade.ts # Thin public API for other features to call
key-rotation.ts # Rotate individual field keys
multi-device.ts # Handle key changes from other sessions
encryption-facade.ts # Thin public API for other features to call
use-vault-timeout.ts # Auto-lock after inactivity (15-min idle timer)
crypto-error-messages.ts # Maps crypto errors to i18n keys
registration-crypto.ts # Pure crypto: deriveRegistrationKeys
ui/
VaultUnlockDialog.tsx
VaultIndicator.tsx
settings/
model/
ui-store.ts # Zustand: settings UI state (expanded sections)
ui/
SecuritySection.tsx # Change password, seed phrase, key versions
SettingsPage.tsx # Settings page shell
PreferencesSection.tsx # Language switcher
AccountSection.tsx # Username, delete account
ChangePasswordDialog.tsx
Expand All @@ -150,6 +159,12 @@ cipher-note-react/
dialog.tsx
sonner.tsx
# etc. — each shadcn component in its own file, imported directly
lib/
network-errors.ts # isNetworkError helper (shared by auth + api adapters)
utils.ts # cn() utility for conditional classes
theme-provider.tsx # NextThemes provider wrapper
use-debounced-value.ts # Debounce hook for search inputs
use-resizable.ts # Resizable panel hook for sidebar
crypto/
aes-gcm.ts # AES-256-GCM encrypt/decrypt/importKey/exportKey
argon2id.ts # Argon2id derivation via argon2-browser bundled build (lazy-loaded)
Expand All @@ -158,7 +173,7 @@ cipher-note-react/
split-kdf.ts # Split KDF (auth + key derivation from password)
mnemonic.ts # BIP-39 generate/validate/wrap/unwrap (lazy-loaded)
crypto-utils.ts # hexEncode, hexDecode, generateIV, generateSalt, generateKey, encodeAAD, zeroFill, copyToUint8Array
key-vault.ts # Module-scoped CryptoKey vault (non-extractable keys)
key-vault.ts # KeyVault class: storeFieldKeys, getKey, unlockVault, lockVault, clearVault
crypto-store.ts # Zustand: loadedFieldKeys, isVaultLocked (memory only, no devtools)
vault-dialog-store.ts # Zustand: vault unlock dialog state (open/close)
api/
Expand All @@ -175,33 +190,33 @@ cipher-note-react/
supabase-adapter.ts # Supabase Auth implementation
auth-context.tsx # React context + useAuth hook (AuthProvider is in features/auth/)
username-utils.ts # toSupabaseEmail, fromSupabaseEmail
password-utils.ts # Password strength validation
# future: custom-jwt-adapter.ts, opaque-adapter.ts
realtime/
realtime.types.ts # IRealtimeAdapter interface
supabase-realtime.ts # Supabase Realtime implementation
# future: ws-realtime.ts
i18n/
config.ts # i18next init + language detector + http backend
locales/
en/
common.json
auth.json
fields.json
settings.json
crypto.json
cs/
common.json
auth.json
fields.json
settings.json
crypto.json
config.ts # i18next initialization + resource bundles
locales/en/ # English translations (auth, crypto, landing)
locales/cs/ # Czech translations (auth, crypto, landing)
types/
crypto.types.ts # AesGcmOptions, RecoveryWrapOptions, WrappedFieldKey, EncryptedField, etc.
api.types.ts # ServerKeys, ServerFieldKey, etc.
entities/
user.types.ts # User entity types
field.types.ts # Field entity types
key.types.ts # Key entity types
api.types.ts # IApiAdapter interface, ApiError types
crypto.types.ts # Crypto primitives, KeyHierarchy, WrappedFieldKey
entities/ # Typed entities: field, key, user
test/
setup.ts # Vitest setup: store resets, mocks
utils.tsx # Custom render with ThemeProvider wrapper
supabase-mock.ts # Supabase client mocks
features/
landing/
ui/
LandingPage.tsx # Landing page shell
HeroSection.tsx # Hero with headline + CTA
FeaturesGrid.tsx # Feature cards
HowItWorks.tsx # Step-by-step explainer
SecurityBanner.tsx # "No credit card" banner
CtaButtons.tsx # Primary/secondary CTA buttons
e2e/
auth.spec.ts
fields.spec.ts
Expand Down
2 changes: 1 addition & 1 deletion docs/implementation-plan/04-phase-4-crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
- Re-wrap master key with new password key using AES-256-GCM (AAD = "master-key-password'")
- Return `{ newAuthHash, newAuthSalt, newKeySalt, newWrappedMasterKey, newMasterKeyIV }`
- `AuthCredentials`, `LoginCredentials`, `PasswordChangeResult` types already exist in crypto.types.ts — no changes needed
- `derive-placeholder.ts` remains in place until Step 21 replaces its consumer in `auth-flow.ts` `loginUser`
- `derive-placeholder.ts` remains in place until Step 21 replaces its consumer in `src/features/auth/model/auth-service.ts` `loginUser`

**Tests:**
- `deriveAuthCredentials`: generates two salts, calls Argon2id with correct args, returns correct types
Expand Down
Loading
Loading