Scaffold YubiKey-gated Proton Bridge rewrite

ober

55a265173a4ec7203f2696971cbf7d69777bc994

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3e59553
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.DS_Store
+*.so
+*.dylib
+*.o
+*.wpo
+*.boot
+*.log
+tmp/
+cache/
+vault.local
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dbfef80
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,5 @@
+This project is intended to be GPL-3.0-or-later compatible because it is a
+Jerboa rewrite informed by Proton Mail Bridge, which is GPL-3.0-or-later.
+
+Add the full GPL-3.0-or-later license text before distributing binaries or
+accepting external contributions.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..037d577
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+JERBOA_HOME ?= $(realpath $(CURDIR)/../jerboa)
+SCHEME      ?= $(JERBOA_HOME)/.chez/bin/scheme
+JERBOA_YUBIKEY_DIR ?= $(realpath $(CURDIR)/../jerboa-yubikey)
+JERBOA_MAIL_DIR ?= $(realpath $(CURDIR)/../jerboa-mail)
+LIBDIRS := $(CURDIR):$(JERBOA_YUBIKEY_DIR):$(JERBOA_MAIL_DIR):$(JERBOA_HOME)/lib
+
+.PHONY: help run test clean
+.DEFAULT_GOAL := help
+
+help:
+	@echo "jerboa-proton-bridge"
+	@echo ""
+	@echo "Development:"
+	@echo "  make run ARGS='--help'  Run the CLI"
+	@echo "  make test               Run smoke tests"
+	@echo "  make clean              Remove local generated files"
+	@echo ""
+	@echo "Environment:"
+	@echo "  JERBOA_HOME = $(JERBOA_HOME)"
+	@echo "  SCHEME      = $(SCHEME)"
+	@echo "  JERBOA_YUBIKEY_DIR = $(JERBOA_YUBIKEY_DIR)"
+	@echo "  JERBOA_MAIL_DIR = $(JERBOA_MAIL_DIR)"
+
+run:
+	JERBOA_HOME=$(JERBOA_HOME) \
+		$(SCHEME) -q --libdirs $(LIBDIRS) --script main.ss -- $(ARGS)
+
+test:
+	JERBOA_HOME=$(JERBOA_HOME) \
+		$(SCHEME) -q --libdirs $(LIBDIRS) --script test/test-all.ss
+
+clean:
+	rm -rf tmp cache
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e3f9e32
--- /dev/null
+++ b/README.md
@@ -0,0 +1,79 @@
+# jerboa-proton-bridge
+
+YubiKey-gated Proton Mail client and Proton Bridge rewrite in Jerboa.
+
+This project is a rewrite/reimplementation effort using Proton Mail Bridge as
+a GPL-3.0-or-later reference, but it is not intended to reproduce the same
+security boundary. The specific goal is to avoid a long-lived local IMAP
+password that allows mail access without a fresh YubiKey-gated authorization.
+
+Upstream reference clone:
+
+```text
+/Users/user/mine/proton-bridge-upstream
+```
+
+Current companion libraries:
+
+- `/Users/user/mine/jerboa-yubikey`
+- `/Users/user/mine/jerboa-mail`
+- `/Users/user/mine/jerboa-pgp`
+- `/Users/user/mine/jerboa-https`
+- `/Users/user/mine/jerboa-crypto`
+
+## Security Model
+
+The upstream Bridge authenticates to Proton, stores session/key material, then
+exposes local IMAP/SMTP with generated credentials. That is useful for normal
+mail clients, but it means local possession of Bridge IMAP credentials is
+enough to read decrypted mail while Bridge is running.
+
+This rewrite starts with stricter rules:
+
+- No password-authenticated local IMAP server in the default mode.
+- No reusable Bridge password equivalent in the default mode.
+- Every interactive mail-reading session must pass a YubiKey gate.
+- The YubiKey gate should use Proton's actual FIDO2/WebAuthn challenge when
+  authenticating to Proton.
+- Any local cache must be encrypted and separately YubiKey-gated.
+- SMTP/send remains out of scope until read-only access is correct.
+
+## Reference Findings
+
+The upstream Bridge clone is Go and uses:
+
+- `github.com/ProtonMail/go-proton-api`
+- `github.com/ProtonMail/go-srp`
+- `github.com/ProtonMail/gopenpgp/v2`
+- `github.com/ProtonMail/gluon`
+- `github.com/ProtonMail/go-libfido2`
+
+Relevant upstream paths:
+
+- `internal/bridge/user.go` - login, 2FA, key unlock, refresh.
+- `internal/fido/` - FIDO2/WebAuthn assertion flow.
+- `internal/vault/` - stored auth/key material.
+- `internal/services/imapservice/` - IMAP-facing service.
+- `pkg/message/` and `pkg/mime/` - message decrypt/build helpers.
+
+## First Target
+
+Build a read-only native Proton CLI:
+
+```sh
+jerboa-proton-bridge login
+jerboa-proton-bridge folders
+jerboa-proton-bridge list --folder INBOX
+jerboa-proton-bridge show --folder INBOX --id MESSAGE_ID
+```
+
+Only after native auth/decrypt works should we decide whether to expose any
+local protocol. If we do, it should be a YubiKey-gated IPC protocol or
+single-user Unix socket, not a reusable IMAP password.
+
+## Development
+
+```sh
+make run ARGS='--help'
+make test
+```
diff --git a/main.ss b/main.ss
new file mode 100644
index 0000000..1534bac
--- /dev/null
+++ b/main.ss
@@ -0,0 +1,29 @@
+#!chezscheme
+;;; Script entry point for jerboa-proton-bridge.
+
+(import (except (chezscheme)
+                make-hash-table hash-table?
+                sort sort!
+                printf fprintf
+                path-extension path-absolute?
+                with-input-from-string with-output-to-string
+                iota 1+ 1-
+                partition
+                make-date make-time))
+
+(define home (or (getenv "HOME") "."))
+(define jerboa-dir
+  (or (getenv "JERBOA_HOME")
+      (string-append home "/mine/jerboa")))
+(define project-dir (current-directory))
+
+(library-directories
+  (append
+    (list (cons project-dir project-dir)
+          (cons (string-append jerboa-dir "/lib")
+                (string-append jerboa-dir "/lib")))
+    (library-directories)))
+
+(import (proton-bridge cli))
+
+(run-cli (command-line-arguments))
diff --git a/proton-bridge/cli.ss b/proton-bridge/cli.ss
new file mode 100644
index 0000000..b11aaa9
--- /dev/null
+++ b/proton-bridge/cli.ss
@@ -0,0 +1,87 @@
+#!chezscheme
+;;; (proton-bridge cli) - command shell for the Jerboa rewrite.
+
+(library (proton-bridge cli)
+  (export run-cli usage-string)
+
+  (import (except (chezscheme)
+                  make-hash-table hash-table?
+                  sort sort!
+                  printf fprintf
+                  path-extension path-absolute?
+                  with-input-from-string with-output-to-string
+                  iota 1+ 1-
+                  partition
+                  make-date make-time)
+          (proton-bridge security))
+
+  (define version "0.0.0-rewrite-plan")
+
+  (define usage-string
+    (string-append
+      "usage: jerboa-proton-bridge <command> [options]\n"
+      "\n"
+      "commands:\n"
+      "  status       Show rewrite/security status\n"
+      "  login        Native Proton login with YubiKey gate (planned)\n"
+      "  folders      List Proton folders (planned)\n"
+      "  list         List messages (planned)\n"
+      "  show         Fetch/decrypt/show one message (planned)\n"
+      "  help         Print this help\n"
+      "  version      Print version\n"))
+
+  (define (println s)
+    (display s)
+    (newline))
+
+  (define (die code msg)
+    (display msg (current-error-port))
+    (newline (current-error-port))
+    (exit code))
+
+  (define (strip-script-separator args)
+    (if (and (pair? args) (string=? (car args) "--"))
+        (cdr args)
+        args))
+
+  (define (print-status)
+    (println "jerboa-proton-bridge")
+    (println (string-append "version: " version))
+    (println "upstream reference: /Users/user/mine/proton-bridge-upstream")
+    (println "security:")
+    (for-each
+      (lambda (item)
+        (println
+          (string-append
+            "  "
+            (symbol->string (car item))
+            ": "
+            (if (cdr item) "yes" "no"))))
+      (security-summary))
+    (println "native Proton auth: planned")
+    (println "FIDO2/WebAuthn via jerboa-yubikey: planned")
+    (println "password-authenticated local IMAP: intentionally disabled"))
+
+  (define (planned command)
+    (die 2
+         (string-append
+           command
+           " is planned; native Proton auth and FIDO2 are not implemented yet")))
+
+  (define (run-cli raw-args)
+    (let ([args (strip-script-separator raw-args)])
+      (cond
+        [(null? args) (display usage-string)]
+        [(or (string=? (car args) "-h")
+             (string=? (car args) "--help")
+             (string=? (car args) "help"))
+         (display usage-string)]
+        [(string=? (car args) "version") (println version)]
+        [(string=? (car args) "status") (print-status)]
+        [(string=? (car args) "login") (planned "login")]
+        [(string=? (car args) "folders") (planned "folders")]
+        [(string=? (car args) "list") (planned "list")]
+        [(string=? (car args) "show") (planned "show")]
+        [else (die 2 (string-append "unknown command: " (car args)))])))
+
+  ) ;; end library
diff --git a/proton-bridge/security.ss b/proton-bridge/security.ss
new file mode 100644
index 0000000..e3f1f6b
--- /dev/null
+++ b/proton-bridge/security.ss
@@ -0,0 +1,45 @@
+#!chezscheme
+;;; (proton-bridge security) - explicit security policy for this rewrite.
+
+(library (proton-bridge security)
+  (export
+    default-security-policy
+    policy-allows-password-imap?
+    policy-requires-yubikey?
+    security-summary)
+
+  (import (except (chezscheme)
+                  make-hash-table hash-table?
+                  sort sort!
+                  printf fprintf
+                  path-extension path-absolute?
+                  with-input-from-string with-output-to-string
+                  iota 1+ 1-
+                  partition
+                  make-date make-time))
+
+  (define (default-security-policy)
+    '((password-imap . #f)
+      (yubikey-required . #t)
+      (smtp-enabled . #f)
+      (plaintext-cache . #f)))
+
+  (define (policy-ref policy key default)
+    (let ([item (assq key policy)])
+      (if item (cdr item) default)))
+
+  (define (policy-allows-password-imap? policy)
+    (policy-ref policy 'password-imap #f))
+
+  (define (policy-requires-yubikey? policy)
+    (policy-ref policy 'yubikey-required #t))
+
+  (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))))
+
+  ) ;; end library
diff --git a/test/test-all.ss b/test/test-all.ss
new file mode 100644
index 0000000..ab3b12d
--- /dev/null
+++ b/test/test-all.ss
@@ -0,0 +1,74 @@
+#!chezscheme
+;;; Smoke tests for jerboa-proton-bridge.
+
+(import (except (chezscheme)
+                make-hash-table hash-table?
+                sort sort!
+                printf fprintf
+                path-extension path-absolute?
+                with-input-from-string with-output-to-string
+                iota 1+ 1-
+                partition
+                make-date make-time))
+
+(define home (or (getenv "HOME") "."))
+(define jerboa-dir
+  (or (getenv "JERBOA_HOME")
+      (string-append home "/mine/jerboa")))
+(define project-dir (current-directory))
+
+(library-directories
+  (append
+    (list (cons project-dir project-dir)
+          (cons (string-append jerboa-dir "/lib")
+                (string-append jerboa-dir "/lib")))
+    (library-directories)))
+
+(import (proton-bridge cli))
+(import (proton-bridge security))
+
+(define failures 0)
+
+(define (pass! name)
+  (fprintf (current-error-port) "  PASS  ~a~%" name))
+
+(define (fail! name detail)
+  (set! failures (+ failures 1))
+  (fprintf (current-error-port) "  FAIL  ~a  (~a)~%" name detail))
+
+(define-syntax check
+  (syntax-rules ()
+    [(_ name expr)
+     (let ([res (guard (e [#t e])
+                  expr)])
+       (cond
+         [(condition? res)
+          (fail! name (condition-message res))]
+         [res (pass! name)]
+         [else (fail! name "returned #f")]))]))
+
+(fprintf (current-error-port) "jerboa-proton-bridge smoke tests~%")
+(fprintf (current-error-port) "=================================~%")
+
+(check "usage mentions status"
+       (let ([needle "status"]
+             [haystack usage-string])
+         (let loop ([i 0])
+           (cond
+             [(> (+ i (string-length needle)) (string-length haystack)) #f]
+             [(string=? needle (substring haystack i (+ i (string-length needle)))) #t]
+             [else (loop (+ i 1))]))))
+
+(check "default policy disables password IMAP"
+       (not (policy-allows-password-imap? (default-security-policy))))
+
+(check "default policy requires YubiKey"
+       (policy-requires-yubikey? (default-security-policy)))
+
+(if (= failures 0)
+    (begin
+      (fprintf (current-error-port) "~%All tests passed.~%")
+      (exit 0))
+    (begin
+      (fprintf (current-error-port) "~%~a test(s) failed.~%" failures)
+      (exit 1)))