| title | Authentication Module | |||
|---|---|---|---|---|
| created | 2026-05-28 | |||
| updated | 2026-05-28 | |||
| type | module | |||
| tags |
|
|||
| sources |
|
The protondrive-linux authentication system is split into two cooperating layers:
- Rust backend (
auth.rs) — SRP cryptographic login, session management, token refresh - JS proxy layer (
main.rsinitialization script) — API interception, cookie sync, CAPTCHA handling, SSO navigation
This document explains both layers and how they interact.
File: src-tauri/src/auth.rs
The auth module implements Proton's SRP (Secure Remote Password) authentication protocol using the official proton-srp crate. It is designed as a standalone module; see Integration Status at the end of this section.
| Type | Role |
|---|---|
AuthManager |
Full lifecycle manager — login, session storage, token refresh, logout |
AuthSession |
Token triplet (uid, access_token, refresh_token, token_type) returned after a successful authentication |
AuthError |
Enum covering all failure modes |
| Variant | Trigger |
|---|---|
Network(reqwest::Error) |
HTTP/transport failure |
Srp(String) |
SRP proof calculation or verification failed |
InvalidResponse(String) |
API returned unexpected or unparseable data |
TwoFactorRequired |
Server requested a TOTP code before granting access |
InvalidCredentials |
Wrong username/password or server-proof mismatch |
NotAuthenticated |
No session held — call login() or set_session() first |
HumanVerificationRequired |
Proton API returned error 9001; interactive CAPTCHA needed |
| Field | Purpose |
|---|---|
uid |
Proton user/session identifier, sent as x-pm-uid on every API request |
access_token |
Short-lived bearer token (~1 hour); sent as Authorization: Bearer <token> |
refresh_token |
Long-lived token for obtaining a new access_token without re-entering credentials |
token_type |
Usually "Bearer"; prepended to the access_token in the Authorization header |
┌──────────┐ ┌────────────┐ ┌──────────────┐
│ User │ │ AuthManager│ │ Proton API │
│(username │ │ (Rust) │ │ mail.proton.me│
│,password)│ │ │ │ │
└────┬─────┘ └─────┬──────┘ └──────┬────────┘
│ login(u, p) │ │
│─────────────────>│ │
│ │ POST /api/auth/v4/info
│ │──────────────────>│
│ │←─────────────────│ {modulus, server_ephemeral,
│ │ │ version, salt, SRPSession}
│ │ │
│ │ SRPAuth::with_pgp(password, version, salt,
│ │ modulus, server_ephemeral) → proofs
│ │ │
│ │ POST /api/auth/v4 │
│ │──────────────────>│ {ClientEphemeral, ClientProof,
│ │ │ SRPSession}
│ │←─────────────────│ {UID, access_token, refresh_token,
│ │ │ token_type, server_proof}
│ │ │
│ │ proofs.compare_server_proof(server_proof) ✓
│ │ │
│ │ [ if 2FA enabled ]│
│ │ → return Err(TwoFactorRequired)
│ │ → store Pending2FA internally
│ │ │
│ │ Store AuthSession
│ │─────────────────│ Ok(AuthSession)
│ │ │
Step 1 — Get SRP parameters: POST /api/auth/v4/info with the username. The server returns the SRP modulus, server ephemeral, version, salt, and a session identifier.
Step 2 — Calculate SRP proofs: The proton-srp crate computes the client ephemeral and client proof from the password and server parameters. Uses the PGP variant (SRPAuth::with_pgp).
Step 3 — Submit auth request: POST /api/auth/v4 with the username, client ephemeral, client proof, and SRP session.
Step 4 — Verify server proof: The server returns its own proof. The client verifies it cryptographically using proofs.compare_server_proof(). If this fails, the credentials are rejected with InvalidCredentials — this catches server impersonation or MITM attacks.
Step 5 — Check for 2FA: If the server indicates TOTP is enabled, the login pauses. The partial tokens are stored as Pending2FA internally, and TwoFactorRequired is returned to the caller.
Step 6 — Store session: On success, the AuthSession is stored in the in-memory RwLock<Option<AuthSession>>.
After receiving a TwoFactorRequired error, call submit_2fa(totp_code):
auth_manager.submit_2fa("123456").await?;This sends POST /api/auth/v4/2fa with the TOTP code, using the pending session tokens. On success, the final AuthSession is stored and returned. The pending state is cleared.
refresh_token() obtains a new access_token without re-entering credentials:
POST /api/auth/v4/refresh
Body: { UID, RefreshToken, ResponseType: "token", GrantType: "refresh_token", RedirectURI: "https://proton.me" }
Uses the refresh_token from the current session. On success, both access_token and refresh_token are updated in memory.
The AuthManager stores sessions only in memory (RwLock<Option<AuthSession>>). Two methods support external persistence:
| Method | Purpose |
|---|---|
get_session() -> Option<AuthSession> |
Read the current session for serialization to disk |
set_session(AuthSession) |
Restore a previously serialized session on launch |
Callers are expected to persist AuthSession to secure storage (e.g., OS keychain, encrypted file) and restore it on startup.
logout() sends DELETE /api/auth/v4 to invalidate the session server-side, then clears the in-memory session. The HTTP request is best-effort (result is ignored) — local state is always cleared.
| Helper | Endpoint | Purpose |
|---|---|---|
get_auth_info(username) |
POST /api/auth/v4/info |
Fetches SRP parameters. Returns HumanVerificationRequired on error code 9001 |
submit_auth(username, client_ephemeral, client_proof, srp_session) |
POST /api/auth/v4 |
Submits SRP proofs. Validates code == 1000 on success |
Note: As of the current codebase,
auth.rsdefines the authentication types and logic but is not yet wired into the binary. The file exists atsrc-tauri/src/auth.rsbut nomod auth;declaration appears inmain.rs, andproton-srpis not listed inCargo.tomldependencies. The actual running application uses a different authentication flow: it loads the Proton WebClients frontend in a Tauri WebView and relies on the proxy request system (described below) to handle authentication transparently through the WebView's own login pages.
When
auth.rsis activated, it will provide native SRP login, session persistence to the OS keychain, and offline-first authentication — reducing reliance on the WebView-based login flow.
While auth.rs is planned for native auth, the current application authenticates by loading Proton's web-based account app (tauri://localhost/account/) in a WebView and proxying all API calls through a shared reqwest::Client with cookie support.
┌─────────────────────────────────────────────────┐
│ Tauri WebView │
│ ┌────────────────────────────────────────────┐ │
│ │ Proton WebClients (account/drive) │ │
│ │ │ │
│ │ fetch("/api/...") → intercepted by │ │
│ │ window.fetch override │ │
│ │ ↓ │ │
│ │ window.__TAURI__.invoke("proxy_request") │ │
│ └───────────────────────────┬────────────────┘ │
│ │ │
└──────────────────────────────┼────────────────────┘
│ IPC
┌──────────────────────────────┼────────────────────┐
│ Rust Backend │ │
│ ┌───────────────────────────┴────────────────┐ │
│ │ proxy_request(command) │ │
│ │ ↓ │ │
│ │ reqwest::Client (cookie_store=true) │ │
│ │ ↓ │ │
│ │ Proton API (mail.proton.me) │ │
│ │ ↓ │ │
│ │ Set-Cookie → store_webview_cookie() │ │
│ │ (routes to WebKit native cookie manager) │ │
│ └────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────┘
The on_navigation handler rewrites external Proton URLs to local tauri://localhost/ URLs:
| External URL | Rewritten To |
|---|---|
https://account.proton.me/login |
tauri://localhost/account/?product=drive |
https://account.proton.me/... |
tauri://localhost/account/... |
https://drive.proton.me/... |
tauri://localhost/... |
/login |
tauri://localhost/account/?product=drive |
account.localhost/u/X/drive/... |
tauri://localhost/u/X/ (post-login redirect) |
This ensures the WebView always loads the bundled WebClients assets from the local Tauri protocol, while the account app's SSO flow works as intended.
The proxy_request Tauri command is the critical bridge:
- Frontend intercepts all API fetches/XHRs via monkey-patched
window.fetchandXMLHttpRequest - API calls are sent to the Rust backend via
invoke('proxy_request', { method, url, headers, body }) - Rust rewrites localhost URLs to the real Proton API (
https://mail.proton.me) - The shared reqwest client forwards requests with cookies merged from both WebKit's native jar and the reqwest cookie jar (via
combined_cookie_header()) - Response
Set-Cookieheaders are routed directly into WebKit's native cookie manager (viastore_webview_cookie()) — WebKit handles all cookie lifecycle (expiry, domain, Secure/HttpOnly flags) exactly as a browser would - Legacy cookie cleanup — when a correctly-scoped AUTH/REFRESH cookie is stored on
mail.proton.me, older builds' blank-domain and host-only cookies are automatically deleted
This WebKit-native cookie approach is important because Proton's WebClients frontend relies on cookie-bearing requests for session continuity. Unlike the older x-set-cookie approach that wrote cookies to document.cookie, the current implementation writes directly to WebKit's cookie manager — this is more reliable across app restarts and avoids race conditions between JavaScript's document.cookie and WebKit's native cookie store.
1. Proxy sends API request → Server returns 422 with Code: 9001
2. Fetch interceptor detects 9001 + HumanVerificationToken in response
3. Saves current login credentials from form (email + password) via
`store_login_credentials` command
4. Navigates WebView to verify.proton.me (top-level, not iframe)
5. User completes hCaptcha challenge
6. POST_MESSAGE from verify page fires `HUMAN_VERIFICATION_SUCCESS`
7. Token stored via `store_verification_token` (single-use, zero-trust)
8. WebView navigates back to tauri://localhost/account/
9. Stored credentials retrieved via `get_and_clear_login_credentials`
10. Credentials auto-filled into the login form and submitted
11. Auth API call is retried — `get_and_clear_verification_token` adds
`x-pm-human-verification-token` + `x-pm-human-verification-token-type`
headers to prevent the 9001 from re-firing
Different Linux packaging formats have varying WebKitGTK Worker support:
| Distro Type | Web Worker Strategy |
|---|---|
appimage, aur |
Native Workers — no override needed |
rpm, deb, flatpak, snap |
window.Worker = undefined — forces Proton's main-thread crypto fallback |
| Unknown | Worker deleted and blocked with a stub — safe default |
This is controlled at build time via the DISTRO_TYPE environment variable.
| Concern | Mitigation |
|---|---|
| Credential exposure | Login credentials are stored in memory-only static variables (PENDING_CREDENTIALS) and cleared after single use. Verification tokens similarly use single-use storage. |
| SRP protocol security | Uses the official proton-srp crate. Server proof is verified client-side to prevent MITM/impersonation. |
| Cookie theft | Cookies stored in WebKit's native cookie manager (RFC 6265 scoping). The shared reqwest cookie jar is in-memory only with no persistence. |
| Session storage | The AuthManager holds sessions in-memory only. External persistence must be implemented by the caller with appropriate encryption (OS keychain recommended). |
| Sync command isolation | Sync-related commands validate the caller's origin — only tauri://localhost and tauri://tauri.localhost are permitted (ensure_sync_command_allowed). |
| XSS surface | The fetch/XHR proxy intercept only applies to API URLs (/api/ in path). Non-API requests pass through normally. |
- Token refresh failure during active use: The
refresh_token()method returnsInvalidCredentialsif the refresh fails. The caller should redirect to the login page. - 2FA interrupted mid-flow:
Pending2FAstate is lost onAuthManagerdrop. If the user does not complete 2FA, they must restart login from scratch. - CAPTCHA credential loss: If the user navigates away during CAPTCHA, saved credentials in
PENDING_CREDENTIALSpersist until retrieved. Restarting the app clears them. - Multiple Set-Cookie headers: Proton may send multiple
Set-Cookieheaders in one response. Each is routed individually to WebKit's cookie manager viastore_webview_cookie(), which applies RFC-6265 path/domain scoping before callingwindow.set_cookie(). - blob: URL downloads: Downloads initiated by the web app (via
window.open(blob:...)or anchor clicks) are intercepted, read asArrayBuffer, and saved to~/Downloadsvia thesave_downloadcommand. - Navigation to
/api/paths: Blocked byon_navigationto prevent iframes from loading API endpoints as documents. API calls must go through the fetch proxy.
Symptoms: Login page shows Proton's loading spinner forever. Never reaches the Drive interface.
Causes:
- Worker instantiation failure — the crypto worker can't initialize. Check console for
The operation is insecure(Firefox WebKitGTK equivalent). - Patch not applied — the
hasModulesSupport()patch inpatches/wasn't applied during build. - Wrong detection method — the patch uses
location.protocol(notwindow.__TAURI__) because__TAURI__initializes after the crypto worker.
Fix:
- Open the DevTools console (Right Click > Inspect Element, or
WEBKIT_INSPECTOR=1) - Look for
The operation is insecure— this confirms the worker can't start - Verify the patch: grep for
location.protocol === 'tauri:'in the built frontend bundle - Rebuild with the correct
DISTRO_TYPEor verify the patch is applied
Symptoms: You have to re-login every time you restart the app.
Causes:
- WebView's persistent storage directory wasn't created or is wrong
WEBKIT_FORCE_SANDBOX=1— sandbox blocks IndexedDB/localStorage writes- WebKit data directory permissions are wrong
Fix:
- Check that the WebView data directory exists:
ls -la ~/.local/share/com.proton.drive/webview-data/ - Verify sandbox is disabled: the app sets
WEBKIT_FORCE_SANDBOX=0at startup — ensure this isn't overridden - Check directory permissions:
stat ~/.local/share/com.proton.drive/webview-data/— should be owned by your user
Symptoms: Login works, but after restart, you're logged out. Auth cookies disappear.
Causes:
- WebView cookie storage is in-memory instead of on-disk
- The persistent data directory path changes between runs (e.g. if
$XDG_DATA_HOMEchanges) - WebKit process is sandboxed and can't write cookies to disk
Fix: See "Session Lost After Restart" above — the same root cause. The WebView data directory must be writable and persistent.
- SSO Authentication — End-to-end SSO flow, CAPTCHA handling, cookie bridge protocol
- Proxy System — The fetch/XHR proxy layer, request construction, error handling
- WebView Integration — Cookie handling, IPC commands, security considerations
- Proton Navigation — URL rewriting, SSO routing, CAPTCHA lifecycle