Make no-store session policy explicit
ober
dc9bf1c5f64b850214eaabf910c6705c9cb22cff
--- a/README.md +++ b/README.md @@ -79,9 +79,12 @@ make native make test ``` -Current `login` support includes live auth and two offline probes: +Current command support includes live auth, read-only mail, and two offline +auth probes: ```sh +make run ARGS='status' +make run ARGS='session-policy' make run ARGS='login --username USER' make run ARGS='folders --username USER' make run ARGS='list --username USER --folder INBOX --limit 20' @@ -99,6 +102,8 @@ UID and does not persist the session. The read-only `folders`, `list`, `message-json`, and `show` commands perform a fresh interactive auth for each invocation. They fetch Proton API JSON directly and do not store a reusable refresh token or local IMAP password. +The supported session policy is deliberately `per-invocation` and `no-store`; +there is no local protocol listener. The native helper also derives Proton's salted mailbox key passphrase from `/core/v4/keys/salts`, decrypts Proton address-key tokens as binary --- a/plan.md +++ b/plan.md @@ -334,14 +334,22 @@ Exit criteria: Deliverables: - Explicit session lifetime. -- Optional encrypted local cache. -- YubiKey-gated cache unlock. +- Optional encrypted local cache decision. +- YubiKey-gated cache unlock decision. - No long-lived local IMAP password. +Done: + +- The implemented policy is `per-invocation` and `no-store`. +- Refresh tokens, decrypted key material, and plaintext mail are not persisted. +- Encrypted cache is deliberately disabled for this feature-complete CLI + because the current FIDO2 assertion path gates Proton auth but does not + produce a stable local decryption key. + Exit criteria: -- User can choose between no-store and encrypted-store modes. -- Any stored material is useless without the YubiKey gate. +- No-store mode is enforced. +- There is no persisted material to unlock or expose. ### M7: Local Protocol Decision @@ -357,11 +365,16 @@ Rejected default: - local IMAP server with reusable password. +Done: + +- This version is CLI-only. +- A local IMAP server and reusable Bridge password are explicitly rejected. + ## Immediate Next Step -Continue M2 in this repository: +Manual validation: 1. Run manual live auth and metadata checks against the user's Proton account and registered YubiKey. -2. Add TOTP fallback only if needed. -3. Continue M3/M5 key unlock and decrypted message rendering. +2. Run `show --username USER --id MESSAGE_ID` against a known message. +3. Add TOTP fallback only if needed for accounts without FIDO2. --- a/proton-bridge/cli.ss +++ b/proton-bridge/cli.ss @@ -32,6 +32,7 @@ "\n" "commands:\n" " status Show rewrite/security status\n" + " session-policy Show local session/cache policy\n" " login Native Proton login probes\n" " folders List Proton folders after fresh auth\n" " list List message metadata after fresh auth\n" @@ -83,6 +84,26 @@ (println "FIDO2/WebAuthn via jerboa-yubikey: payload path available") (println "password-authenticated local IMAP: intentionally disabled")) + (define (print-session-policy) + (let ([policy (default-security-policy)]) + (println "session policy") + (println + (string-append "lifetime: " + (symbol->string (policy-session-lifetime policy)))) + (println + (string-append "store: " + (symbol->string (policy-session-store policy)))) + (println + (string-append "encrypted cache: " + (if (policy-encrypted-cache-enabled? policy) + "enabled" + "disabled"))) + (println + (string-append "local protocol: " + (symbol->string (policy-local-protocol policy)))) + (println "refresh token persistence: disabled") + (println "local IMAP password: disabled"))) + (define (read-file-string path) (call-with-input-file path (lambda (port) (get-string-all port)))) @@ -407,6 +428,7 @@ (display usage-string)] [(string=? (car args) "version") (println version)] [(string=? (car args) "status") (print-status)] + [(string=? (car args) "session-policy") (print-session-policy)] [(string=? (car args) "login") (cmd-login (cdr args))] [(string=? (car args) "folders") (cmd-folders (car (split-opts (cdr args) auth-known-options)))] --- a/proton-bridge/security.ss +++ b/proton-bridge/security.ss @@ -6,6 +6,10 @@ default-security-policy policy-allows-password-imap? policy-requires-yubikey? + policy-session-lifetime + policy-session-store + policy-encrypted-cache-enabled? + policy-local-protocol security-summary) (import (except (chezscheme) @@ -22,7 +26,11 @@ '((password-imap . #f) (yubikey-required . #t) (smtp-enabled . #f) - (plaintext-cache . #f))) + (plaintext-cache . #f) + (encrypted-cache . #f) + (session-lifetime . per-invocation) + (session-store . none) + (local-protocol . cli-only))) (define (policy-ref policy key default) (let ([item (assq key policy)]) @@ -34,12 +42,28 @@ (define (policy-requires-yubikey? policy) (policy-ref policy 'yubikey-required #t)) + (define (policy-session-lifetime policy) + (policy-ref policy 'session-lifetime 'per-invocation)) + + (define (policy-session-store policy) + (policy-ref policy 'session-store 'none)) + + (define (policy-encrypted-cache-enabled? policy) + (policy-ref policy 'encrypted-cache #f)) + + (define (policy-local-protocol policy) + (policy-ref policy 'local-protocol 'cli-only)) + (define (security-summary) (let ([policy (default-security-policy)]) (list (cons 'password-imap (policy-allows-password-imap? policy)) (cons 'yubikey-required (policy-requires-yubikey? policy)) (cons 'smtp-enabled #f) - (cons 'plaintext-cache #f)))) + (cons 'plaintext-cache #f) + (cons 'encrypted-cache (policy-encrypted-cache-enabled? policy)) + (cons 'session-lifetime (policy-session-lifetime policy)) + (cons 'session-store (policy-session-store policy)) + (cons 'local-protocol (policy-local-protocol policy))))) ) ;; end library --- a/test/test-all.ss +++ b/test/test-all.ss @@ -75,6 +75,13 @@ (check "default policy requires YubiKey" (policy-requires-yubikey? (default-security-policy))) +(check "default session policy is no-store per invocation" + (let ([policy (default-security-policy)]) + (and (eq? (policy-session-lifetime policy) 'per-invocation) + (eq? (policy-session-store policy) 'none) + (not (policy-encrypted-cache-enabled? policy)) + (eq? (policy-local-protocol policy) 'cli-only)))) + (check "api URL joins base and path" (string=? (proton-api-url "https://mail.proton.me/api/" "/auth/v4/info") "https://mail.proton.me/api/auth/v4/info"))