Files
rclone/docs/specs/001-protondrive-tier1.md
T
Lucas WintherandClaude Fable 5.2 4282540e02
build / windows (push) Skipped
build / other_os (push) Skipped
build / mac_amd64 (push) Skipped
build / mac_arm64 (push) Skipped
build / linux (push) Skipped
build / go1.26 (push) Skipped
build / linux_386 (push) Skipped
build / lint (push) Skipped
build / android-all (push) Skipped
Build & Push Docker Images / Build Docker Image for linux/386 (push) Skipped
Build & Push Docker Images / Build Docker Image for linux/amd64 (push) Skipped
Build & Push Docker Images / Build Docker Image for linux/arm/v6 (push) Skipped
Build & Push Docker Images / Build Docker Image for linux/arm/v7 (push) Skipped
Build & Push Docker Images / Build Docker Image for linux/arm64 (push) Skipped
Build & Push Docker Images / Merge & Push Final Docker Image (push) Waiting for needed jobs
docs: correct the Proton Drive handoff after a cold read
A reader with only the repository walked the handoff and the remaining
plan tasks against the code. Mark the plan and spec lines that later
rulings superseded, add corrections for Tasks 18 to 21, fix stale TODO
and PERMISSIONS entries, ignore the skill workspace, point AGENTS.md at
the handoff and take the test account usernames out of it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 21:21:43 +01:00

44 KiB

status, date
status date
approved 2026-11-11

001 - Proton Drive: a maintainable, SDK-conformant backend

Fork-only document. Approved by the fork owner on 2026-11-11 with the decisions recorded in section 14. Decisions marked (call) were made by the author of this spec and accepted with it; none has been agreed with rclone's maintainers.

Scope decided by the owner: get a working backend in this fork first. Nothing is posted upstream for now, the owner is not offering permanent maintainership, and the fake server stays in the fork. The design still follows rclone's conventions so that upstreaming remains possible later.

Implementation status (2026-11-11): paused after plan Task 17 of 21. M1 to M3 are built; M4's swap is done (the backend runs on the native client and passes the contract suite against the fake) but its remaining parts - wider suites, removal of the old libraries, user documentation and live verification - are not. M5 to M7 are not started. State, rulings and how to resume: docs/plans/001-protondrive-tier1-HANDOFF.md.

1. Intent

Rclone's Proton Drive backend works today, but on borrowed time and on one person's goodwill:

  • It is Tier 4, "Community" maintained. Nick Craig-Wood (ncw) is the only committer to the two libraries it depends on, rclone/Proton-API-Bridge and rclone/go-proton-api. Those forks have 15 open, unreviewed pull requests between them, including the fix for a permanent upload deadlock (rclone #10224).
  • Proton has put dates on the current approach. The /drive/shares/{shareId}/links|folders|files|events routes the bridge uses are to be removed within 12 months of 2026-09-16. Uploads only succeed because Proton exempts rclone from the mandatory block verification step, and that exemption ends once third parties have had a chance to adopt the SDK. A breaking cryptography change is targeted for end 2026 / early 2027.
  • Proton's stated rule for third parties is: adopt the official SDK, or "fully emulate its safety, correctness, and availability features". Proton will not ship or accept a Go SDK (ProtonDriveApps/sdk#19 was closed in two days), and names rclone users as its top API consumers "by far".
  • Rclone will not take cgo (ncw, #9252). The official SDK exists only in C# and TypeScript.

The intent is to replace the backend's internals with a native Go implementation that follows the official SDK's behaviour closely enough that Proton has no reason to throttle or block it, lives entirely inside the rclone tree so rclone's maintainers can maintain it like any other backend, and comes with enough hermetic testing that a change can be trusted without a Proton account.

2. Approach, and what was rejected

Chosen: a native Go port inside backend/protondrive, using the official TypeScript SDK as the behavioural reference, built on lib/rest, lib/pacer, lib/dircache, fs/fshttp, and Proton's own MIT Go crypto libraries (ProtonMail/gopenpgp/v3, ProtonMail/go-srp). The two bridge libraries are removed from go.mod.

Why this one:

  • It is the only option that satisfies both hard constraints at once: no cgo (rclone) and SDK-equivalent behaviour (Proton).
  • It fits what CONTRIBUTING.md asks of a backend: lib/rest, the fshttp transport, the layout of backend/box, API types in api/types.go.
  • It removes two external repositories that one maintainer carries alone. ncw has already asked Proton, unanswered, "do you think we should start from scratch and port the c# SDK to Go?" (forum thread 53609, post 12).
  • Proton staff confirmed gopenpgp v3 supports the current format and is used by official clients. The SDK is MIT, so porting from it is licence-clean.
Rejected Why
C# SDK through its C interface Needs cgo. Proton calls the C interface "implementation detail". No rename, copy or revisions. NativeAOT does not cover rclone's mips, BSD, plan9, solaris or aix targets.
TypeScript SDK in an embedded JS or WASM runtime (wazero) ncw's only stated opening, but never done in rclone or anywhere else. The SDK's crypto dependency @protontech/crypto is GPL-3.0, which cannot ship in MIT rclone. Needs a large Web API polyfill surface.
Sidecar process running the official SDK Uses the real SDK, but rclone has no precedent for shipping, downloading or auto-spawning a helper binary; it breaks librclone and mobile; ncw has not answered the proposal in #9252. Stays a fallback if Proton ever blocks non-SDK clients outright.
Keep patching the bridge forks Lowest friction per patch, and what happens today. But the bridge is built on the routes being removed, has no event or cache model, and its review queue is the bottleneck. It prolongs the situation this spec exists to end.
A separate community Go SDK module that rclone imports Reusable by others, but recreates the external-repo maintenance problem and the replace-directive friction ncw rejected in #9260. The port is instead kept free of fs types below a clear seam (section 5) so it can be extracted later. (call)
Build on clayliddell/proton-drive-go-sdk Thin layer over go-proton-api with gopenpgp v2, a deprecated move route, no events, cache or backoff, and test defaults that spoof a first-party app version. Useful to read, not to depend on.

3. Goals

  1. Same backend, same config. Backend name protondrive, every existing config key, existing saved sessions and the default filename encoding keep working with no user action.
  2. SDK conformance. Only the routes the reference SDK itself calls (mostly drive/v2/volumes/..., plus the few unversioned ones it still uses such as drive/blocks and drive/volumes/{id}/trash), and none of the deprecated share-scoped link, folder, file or event routes; block verification tokens; the SDK's draft, revision, manifest and extended-attribute formats; its retry, backoff and parallelism limits; an honest external-drive-rclone@... app version.
  3. Proton-friendly traffic. Metadata caching and event-driven invalidation, so a repeat run does not re-walk the tree.
  4. Meet every code-level Tier 1 attribute in docs/content/tiers.md: feature score of at least 5/7, all integration tests green with nothing ignored, pacer-based error handling, hash and modtime integrity, full docs.
  5. Fix the recurring failures listed in section 12.
  6. Hermetic tests. The full backend contract suite runs against an in-process fake Proton API on every commit, with no credentials.
  7. Fewer dependencies and no larger binary than today.
  8. Upstreamable. Reviewable commits, rclone conventions, no fork-only material in the upstream branch.

4. Non-goals and anti-goals

Non-goals for this spec (the design must not preclude them):

  • Shared with me (#7918), Photos (#7832), Devices/Computers, public links, sharing management, thumbnails, revision history commands.
  • OpenChunkWriter / multipart upload (F7). Blocks are independent, but the revision needs a SHA1 of the whole plaintext, which cannot be computed from out-of-order chunks. (call)
  • ListR. Proton asks clients not to do recursive traversals.
  • The post-2026 "mostly symmetric" crypto model. It is unpublished. Section 5 isolates crypto so it can be added without touching the rclone-facing code.
  • Exporting native Proton Docs and Sheets.

Anti-goals - any of these makes the work a failure even if tests pass:

  • Requiring cgo, a helper binary, or a replace directive.
  • Identifying as a first-party client or as the official SDK (no spoofed x-pm-appversion, no x-pm-drive-sdk-version header - Proton asked for the latter explicitly in #9260).
  • Writing data the official web, desktop or mobile clients cannot read (the v1.75.0 incident, #9844).
  • Decrypted names, keys or passwords written to disk or to logs.
  • Backend-specific code in fs/ or vfs/.
  • Silently changing how existing remote files map to rclone paths.

5. Architecture

One Go package, backend/protondrive, laid out like other multi-file backends (internxt, pikpak). (call) The only sub-packages are api (wire types) and protontest (the fake server of section 9.2, imported only by tests); the seam is by file and by a rule.

File Responsibility May import fs?
protondrive.go Fs, Object, options, features, dircache glue, error mapping to fs sentinels yes
api/types.go Wire request/response structs and API error type no
session.go Login (browser fork, SRP), token refresh, credential persistence, app version, server-time tracking yes (config only)
account.go User, addresses, key salts, unlocking user and address keys, public-key lookup cache no
crypto.go Every OpenPGP operation: node keys, passphrases, names, name hash, content keys, blocks, manifest, extended attributes no
nodes.go Volume/share bootstrap, node fetch and decrypt, children listing, create folder, rename, move, copy, trash, delete no
upload.go Draft, verification, block pipeline, commit, cleanup no
download.go Block listing, verification, decryption, ranged reads no
events.go Event cursor, polling, cache invalidation, change notification no
cache.go In-memory and persistent metadata cache no

The seam rule: protondrive.go contains no cryptography and no URL paths; the other files do not return or accept fs.Object/fs.Fs. This is what lets the new crypto model land in crypto.go plus wire types, and what keeps a later extraction into a standalone module possible.

No package-level mutable state: sessions, keys, caches and the events poller all hang off Fs. (The current _mapper and _saltedKeyPass globals cross-contaminate two Proton remotes in one process.) The single exception is one package-level mutex that serialises key unlocking across Fs instances, which is the fix ncw suggested for #9816. There is no process-wide session or key cache; rclone's own fs/cache already shares an Fs between users of the same remote and root. (call)

Reference pin. A constant-free note in the package doc comment records the SDK tag the port was last checked against (starting at js/v0.23.0). docs/specs/ gets a short conformance checklist per SDK module; re-checking it against new SDK tags is a recurring maintainer task.

Data flow

rclone core -> Fs/Object (protondrive.go)
                 | dircache: path -> node ID
                 v
              nodes / upload / download  <->  cache  <-  events poller
                 |            |
              crypto.go    lib/rest + pacer (API host)   lib/rest (storage host)
                 |            |
              gopenpgp v3  fshttp transport -> drive-api.proton.me

6. Behaviour

6.1 Authentication and sessions

  • Browser sign-in is the default for new remotes. (As built: only when username and password are not both set, so scripted setup does not wait on a browser; and rclone config on a remote with a saved session asks before signing in again.) rclone config runs the session-fork flow used by Proton's official CLI: request a fork, show the user a URL to open on any device, poll, decrypt the returned key password. Client ID external-drive. Password, 2FA and CAPTCHA are handled by Proton's page, and rclone never sees or stores the password. Implemented as a Config state machine like backend/internxt. Upstream PR #9522 proposes the same flow on the old stack; coordinate with its author.
  • Password sign-in stays for existing configs and headless automation: username, password, mailbox_password, 2fa, otp_secret_key, via SRP. A CAPTCHA response (code 9001) produces an error that tells the user to run rclone config reconnect and use browser sign-in.
  • Saved sessions keep working. The existing client_uid, client_access_token, client_refresh_token and client_salted_key_pass keys are read and written with their current meaning, so an upgraded rclone does not force a re-login and a downgrade still works.
  • Refresh is safe across Fs instances and processes. Refresh tokens are single-use. Before refreshing, re-read the config and adopt a newer token if another instance already refreshed, following lib/oauthutil's reReadToken pattern that ncw asked for in #9881. Refresh is single-flight per Fs. A failed refresh never wipes credentials that another process may have just rotated.
  • Key unlock is serialised. Unlocking user and address keys is expensive (#9816: concurrent rcd requests were OOM-killed). It happens once per Fs, the result is held for the life of that Fs, and unlocks in different Fs instances run one at a time, so peak memory is one unlock.
  • Server time. The Date header of API responses sets the clock used for key generation and signature checks, as the official CLI does. This is the fix for code 200501 on machines with a fast clock.
  • Third-party notice. Proton requires apps that prompt for credentials to say they are not official. The config prompt and the docs page carry: "rclone is a third-party application not officially supported by Proton."
  • App version. external-drive-rclone@<semver>-<channel>. Proton's published grammar allows only the channels stable, beta and alpha; today's code sends -dev for development builds. Development builds map to alpha. (call) - to be confirmed with Proton in step 13.5. The app_version option stays for compatibility but its help text says misuse can get the client blocked.
  • Test endpoint. A hidden advanced option endpoint overrides the API host so tests can reach the fake server. It is not documented for users: Proton's rules forbid pointing a client at other domains. (call)

6.2 Listing, cache and events

  • Listing a folder fetches child IDs, then node metadata in batches of up to 100, and decrypts names with the parent's key. ListP is implemented so large folders stream.
  • In-memory cache of decrypted nodes and node keys per Fs, replacing the bridge cache. enable_caching keeps its meaning.
  • Events. While an Fs is alive, volume events are polled no more often than every 30 seconds (Proton's stated floor), and only when the cache is non-empty or ChangeNotify is active. Events invalidate cache and dircache entries, and feed ChangeNotify so mount and serve see remote changes. This removes today's "cache is never invalidated" caveat.
  • Persistent metadata cache (later milestone, opt-in first). Stores the server's encrypted node records and the event cursor on disk through lib/kv. Nothing decrypted is ever persisted. On start, the backend replays events since the cursor instead of re-listing. A tree_refresh event, an expired cursor (about a month) or any decode error drops the cache. lib/kv is unavailable on plan9 and js; there the option is a no-op. (call) This is the piece that answers Proton's main complaint and the "17 minutes for a no-op copy" reports.
  • Native Proton Docs/Sheets nodes have no content blocks. They are left out of listings, with one NOTICE per run. (call) (Today they segfault or fail every sync: #7959.)
  • A node whose name cannot be decrypted is skipped with an ERROR log and counted, never fatal for the whole listing.

6.3 Upload

Follows the SDK's upload module step for step:

  1. Create a draft in the parent with the encrypted name, name hash, node key, passphrase, content key packet and a stable per-remote draft client ID (new hidden config key draft_client_uid; the session UID changes on re-login and so cannot be used). (call)
  2. Name conflict (code 2500): if it is a leftover draft from this client ID, delete it and retry once. If it is another client's draft, fail unless replace_existing_draft is set. If it is a real file, Update creates a new revision of that node. This ends the orphan-draft loop (#9622, #9132).
  3. Fetch the verification code; for every 4 MiB block: encrypt, sign, SHA-256, check the block decrypts, compute the verifier token.
  4. Request upload URLs in batches; upload blocks to the storage host with at most upload_concurrency in flight per file (new advanced option, default 4, max 5 as in the SDK), and at most 10 blocks in flight across the whole Fs (the SDK's client-wide budget of about 40 MiB). With a high --transfers, extra transfers wait their turn; they do not fail. (call) Blocks are buffered from lib/pool so a failed block is re-sent from memory. A storage 404 means the URL expired: request a new one. Every wait is bounded by the context; no path can block forever (#10224).
  5. Before commit, check block count and byte total against what was read, and the SHA1 against the source's if it has one.
  6. Sign the manifest and commit with extended attributes: modification time, size, block sizes, SHA1. If the commit call errors, check whether the revision is in fact active before reporting failure.
  7. On any failure or cancellation, delete the draft.

The SDK's single-request path for files under 128 KiB (files/small, gated by a server feature flag) is not part of the first milestones; it is added in M6 as a request-count optimisation. (call)

Update on an existing object creates a new revision of the same node, so the node ID and history survive. PutStream is supported: unknown size simply omits the intended size. The output format is exactly what the reference SDK tag writes (v4 node key, v6 PKESK content key, SEIPDv2 AES-GCM blocks; names, passphrases, hash keys and attributes without AEAD).

6.4 Download

  • Block metadata is paged. Each encrypted block's SHA-256 is checked against the server's value before decryption; a mismatch is a retryable error.
  • Ranged reads map an offset to a block using the recorded block sizes, so NoMultiThreading is dropped and multi-thread downloads work. At most 10 blocks are in flight for download across the Fs (SDK value).
  • A full, sequential read lists every block and verifies the manifest signature over the listed hashes before releasing the first byte; each block is then checked against its listed hash as it arrives, and a block listed again (for a new URL) with another hash is an integrity error. Failure is an error. An advanced option ignore_signature_errors downgrades content and authorship signature failures to a logged ERROR for users with legacy or shared data (#8003, #7344). (call) A range covering the whole file from offset 0 (as bytes=0-) is checked as a full read, still returning only the bytes asked for. Other ranged reads check each block against its listed hash but cannot verify the manifest; this is documented, as it is in the SDK.
  • A file may list at most 2^19 blocks (2 TiB in 4 MiB blocks, more than any one file a Proton plan can hold), whatever its unsigned size fields claim, which bounds how many blocks' metadata a full read holds before the manifest check (about 250-350 MiB at the cap with realistic tokens and URLs; their length, and API response bodies, aren't bounded yet).
  • A server serving an earlier revision whole (blocks, manifest and extended attributes, all validly signed) under the current revision ID is undetectable: nothing signed binds them to the revision. The SDK cannot detect it either; it is a protocol limit to raise with Proton.
  • Old-format files (pre-AEAD) remain readable.
  • Close releases everything; no reader outlives its Object.Open caller.

6.5 Other operations

Operation Behaviour
Mkdir Create folder; modification time set at creation
Rmdir Trash an empty folder; fs.ErrorDirNotFound when missing. Emptiness is asked of the server, uncached, counting every child including ones rclone does not list
Remove, Purge Move to trash, as today (Proton has no direct permanent delete)
CleanUp Empty trash
Move, DirMove Server-side move and rename with re-encryption to the new parent
Copy Server-side copy (new; SDK copyNodes)
About Used and total space
SetModTime fs.ErrorCantSetModTime - the time lives in the revision
Hashes SHA1 (claimed digest from extended attributes)
Precision 1 second, unchanged (call)
Metadata (later milestone) Read mtime, btime, content-type; write mtime and content-type at upload
Shutdown Stop the event poller, flush the persistent cache. Never clears keys: the Fs stays usable, because rclone's fs cache shuts down entries that rc jobs may still hold
Disconnect Log out and clear saved session keys; later use fails with a "not signed in" error naming the remote

Feature score target: Hash, Modtime, Stream upload, Copy/Move, DirMove = 5/7 at the first upstream milestone; Metadata makes 6/7.

6.6 Errors, retries, pacing

  • Separate pacers for the API host and the storage host, so block uploads under --bwlimit do not starve metadata calls (#9832).
  • 429: honour Retry-After through pacer.RetryAfterError.
  • 5xx and network errors: bounded retries with backoff, mirroring the SDK's limits. 4xx is not retried except 408, 429 and the storage-URL 404.
  • Node-metadata batch fetches run at most 15 at a time (SDK value).
  • API codes map to rclone errors in one table: 2501 to fs.ErrorObjectNotFound/fs.ErrorDirNotFound; quota and limit codes (200001-200004, 200300, 200301) to no-retry errors with Proton's message; 2000/2028 ("app version not supported", unmet requirements) to a fatal error telling the user to upgrade rclone.
  • The x-pm-drive-requirements response headers are logged at debug level so a pending server-side enforcement is visible in bug reports.

6.7 Options

Kept unchanged: username, password, mailbox_password, 2fa, otp_secret_key, the four client_* keys, encoding, original_file_size, app_version, replace_existing_draft, enable_caching.

Added (all advanced): upload_concurrency, ignore_signature_errors, metadata_cache (persistent cache, default off until proven), draft_client_uid (hidden), endpoint (hidden, tests only).

Filename rules: non-empty, at most 255 characters, no /. The encoding default does not change. If rclone test info against the new code shows the default is wrong, that is raised as a separate, explicitly compatibility-reviewed change.

7. Security

  • Threat model includes the server. End-to-end encryption means a compromised or malicious server must not be able to feed rclone altered content undetected: encrypted-block hashes, manifest signatures, name and passphrase signatures are verified as the SDK verifies them. Server-supplied names containing /, .. or NUL are rejected, never joined into paths. Trust is rooted in the user's own addresses, whose keys the account holds: the My Files share must be created, and its root folder signed, by one of them, else everything is treated as unverified (readable and writable only with ignore_signature_errors). A key that isn't verified makes everything beneath it unverified, and rclone never moves, copies, renames or writes into such nodes without the option. Limit: a file or folder signed under another user's email is checked against public keys the server itself supplies, so a hostile server can forge those; Proton's SDK shares this limit, and key transparency or address-key pinning is the eventual answer.
  • Least privilege, as far as Proton allows. Proton offers no scoped tokens (sdk#7), so a session is a full account session. Browser sign-in means the password is never held; that is the recommended setup in the docs. The key password and tokens stay in the rclone config, which the docs tell the user to encrypt.
  • No secrets in logs. The backend's own log lines never contain passwords, key passwords, tokens, session keys, decrypted passphrases or upload tokens, at any -v level. This is tested (section 9.6). rclone's --dump flags are a different matter: fshttp redacts only the Authorization header, Proton carries tokens in JSON bodies and in the pm-storage-token header, and changing fshttp is out of scope. The docs page says plainly that --dump output for this backend contains secrets.
  • Nothing decrypted on disk. The persistent cache holds ciphertext only.
  • No telemetry is sent to Proton or anyone else.
  • TLS is rclone's standard fshttp transport, without certificate pinning, like every other backend. (call)
  • Supply chain. Two forked modules and their transitive dependencies (resty, gluon, go-message and others) leave go.mod. gopenpgp/v3 and go-srp become direct dependencies; both are Proton-maintained and already in the build.

8. Constraints

  • Pure Go; builds on every target of bin/cross-compile.go. No new _unsupported.go stubs.
  • Go 1.26 minimum, as go.mod.
  • All HTTP through fs/fshttp, so --dump, --tpslimit, proxies and the test fault injector apply.
  • Memory: at most 10 upload blocks of 4 MiB (+ ciphertext overhead) buffered per Fs, taken from lib/pool and always returned.
  • rclone code style, comment style and commit message rules from AGENTS.md. Docs option sections are generated, not hand-edited.
  • CONTRIBUTING's AI policy: the human submitter must understand every line. Work is therefore delivered in small commits a person can actually review.

9. Testing

Testing is the half of this project that makes the other half maintainable. There are seven layers; 9.1 to 9.4 run on every commit with no network.

9.1 Unit tests with fixed vectors

Table-driven tests for every function in crypto.go, session.go and the error mapping:

  • name hash (HMAC-SHA256, including names with non-ASCII, combining characters and the #9622 failing shapes);
  • key password derivation (bcrypt output slice);
  • fork payload decryption (AES-256-GCM, AAD fork);
  • verifier token (XOR over code length, zero padding, block shorter than code);
  • manifest construction (thumbnail-before-block ordering);
  • extended attribute encode/decode, including unknown extra fields preserved;
  • node key, passphrase, content key and block round trips, and reading pre-AEAD fixtures;
  • app version formatting; API code to error mapping; retry decision table.

Vectors come from two sources so the port cannot merely agree with itself. The SDK's mirrored unit tests (88 files under client/js) supply cases for the non-cryptographic logic: extended attributes, verifier, manifest ordering, draft conflicts, error mapping, retry. They mock OpenPGP, so they are not a crypto oracle. That role falls to golden fixtures captured once from a throwaway Proton account - raw API responses plus the account's test-only keys - for nodes created by the official web app, the official CLI and the old rclone backend. Fixtures live in backend/protondrive/testdata/ and contain no real user data.

Until a test account exists, the crypto oracle is differential testing against the old libraries: while rclone/Proton-API-Bridge and rclone/go-proton-api are still in go.mod (they pass rclone's live integration tests today), test-only code uses them to produce keys, names, passphrases, content keys, blocks and manifests that the new code must read, and to read what the new code writes. These tests are deleted in the M4 commit that removes the libraries; by then golden fixtures must have replaced them. No milestone is called done on differential tests alone where a live check is listed for it.

9.2 Fake Proton server

backend/protondrive/protontest is a stateful httptest implementation of the routes the backend uses, importable by tests and reached through the hidden endpoint option: fork and SRP auth, refresh, user/addresses/salts/keys, volumes and shares, node fetch, children, folders, files, revisions, verification, block URLs, storage upload/download, rename, move, copy, trash, events.

It does not trust the client. It is strict where the real service is strict:

  • rejects a block whose verifier token is wrong;
  • rejects a commit whose manifest does not match the uploaded block hashes or whose block count/size is inconsistent;
  • enforces name-hash uniqueness per folder (code 2500 with conflict details) and draft ownership;
  • issues single-use refresh tokens and expiring storage URLs;
  • returns 410 for every legacy /drive/shares/{id}/links|folders|files|events route, so a regression onto removed routes fails immediately;
  • rejects requests whose app version does not match Proton's published grammar for external-drive-rclone@, and any request carrying x-pm-drive-sdk-version;
  • counts every request by route.

It can be scripted to inject: 429 with Retry-After, 5xx, connection resets, slow responses, 401 mid-operation, expired storage URLs, a commit that succeeds but returns an error, CAPTCHA 9001, a skewed Date header, and hostile responses (9.6).

ProtonMail/go-srp ships server-side SRP (server.go), which the fake uses for password login.

9.3 Full contract suite, hermetically

fstests.Run runs against the fake server on every go test, with QuickTestOK: true and a generated in-memory remote config. No network backend in rclone does this today; it means make quicktest exercises encoding, ranges, retries (FsPutRetry and its pool-balance assertion), aborted uploads leaving nothing behind (FsPutError), PutStream, Copy, Move, DirMove, Purge, ChangeNotify and Shutdown for Proton Drive.

The same tests run with -race. In fork CI only, the fs/operations, fs/sync and vfs suites also run against the fake: a small main under protontest serves it standalone, started by a fstest/testserver/init.d/TestProtonDriveFake script like the other test servers. That script and remote are fork-only unless upstream wants them.

9.4 Behaviour and regression tests against the fake

One named test per failure this project exists to fix. Rows about events belong to M5 and rows about the persistent cache to M6; everything else is due by M4.

Test Guards
Block upload gets a 502, then succeeds; and gets 502 forever #10224 - completes, or fails within a bound; never hangs; no goroutine left
Upload fails after draft creation, then is retried #9622/#9132 - no orphan draft, second attempt succeeds
Commit returns an error but the revision is active no duplicate, reported as success
Two Fs on one remote, and two processes on one config file, refresh at once #9880/#8135 - both keep working, credentials never wiped
Many NewFs calls concurrently #9816 - unlocks never overlap; memory bounded to one
Password with spaces, quotes, non-ASCII, 72+ bytes #8993/#9207
Server clock ahead/behind by hours 200501
CAPTCHA on password login clear, actionable error
Folder and file created "by the SDK" fixtures #9622 name-hash lookup
Docs node, node with undecryptable name, node with bad signature #7959, #8003 - skip or warn, never crash
File overwritten by directory and the reverse #9438
Context cancelled at every stage of upload and download no leak, draft removed, pool balanced
Events: created, updated, deleted, moved, tree_refresh, cursor expired cache, dircache and ChangeNotify all correct
Persistent cache: corrupt file, wrong account, old schema dropped and rebuilt, never trusted

Request budgets. Using the fake's counters, tests assert upper bounds that encode Proton's traffic expectations, for example:

  • listing a folder of N files costs at most P + ceil(N/100) + 1 requests, where P is the number of child-ID pages the server returned;
  • a second listing with a warm cache costs zero metadata requests;
  • a no-op sync of a tree with D folders and N files, cold, stays within a stated linear bound, and with the persistent cache warm costs a constant number of requests;
  • an idle Fs makes at most one events request per 30 seconds, and none when nothing is cached or watching.

The bounds are written down when the code first meets them and only ever tightened.

9.5 Fuzzing

Go native fuzz targets for everything that parses server-controlled bytes: extended attributes, API error bodies, fork payloads, node decoding, event decoding. Seeded from the golden fixtures; a short fuzz run is part of fork CI, long runs are manual.

9.6 Security tests

  • Hostile server. The fake returns: a block with a wrong hash; blocks swapped or truncated; a block list shorter than the manifest; an older revision's blocks under the current revision; a block encrypted with another key; a name decrypting to ../x, a/b or containing NUL; a 10 MiB "name"; an attacker-signed passphrase; a folder with a server-made key and anonymous files beneath it. Each must produce an integrity error, not data: a full read releases no byte before the error. (A whole earlier revision served under the current ID is undetectable; see 6.4.)
  • Secret hygiene. A test runs login, upload and download at -vv (without --dump), then searches the captured log for the known password, key password, tokens, session keys and upload tokens. A second test checks that after browser sign-in the config holds no password, and that after password sign-in the password is stored only obscured, as today. (Session tokens and the key password are in the config by design.)
  • Persistent cache (M6). After a full run, the cache file contains none of the plaintext file names used in the test.
  • govulncheck ./... and golangci-lint run clean, as in upstream CI.

9.7 Live tests

Against real Proton accounts dedicated to testing:

  • Contract and integration: go run ./fstest/test_all -backends protondrive clean, with no ignore: entries, and with the cmd/bisync and cmd/gitannex exclusions removed from fstest/test_all/config.yaml once they pass. This run's report goes in the upstream PR, as CONTRIBUTING requires.
  • Account matrix: free account; paid account; two-password mode; custom-domain address (#9434); account with 2FA; an account holding pre-2024 data. Whatever cannot be obtained is listed as untested in the PR.
  • Interop with official clients, both directions. Upload with rclone, download with Proton's official CLI and compare; create folders and files with the official CLI, then list, read, overwrite, move and delete them with rclone; a manual checklist confirms the web app opens rclone-written files and shows no signature warnings. Automated behind an environment variable because it needs the CLI binary and an interactive first login.
  • Upgrade and downgrade: a config and a tree created by the current release are used by the new code without re-login, and the released version still reads what the new code wrote.
  • Scale and soak: 10,000 small files; one 5 GiB file; rclone mount under a read/write workload for an hour while another client changes the tree; a restic and a bisync session. Record request counts, 429 counts, peak memory and wall time.
  • Benchmark against the tier's "within 2x S3" line using the same workloads on an S3 remote; publish the numbers in the PR even where Proton's block and crypto model makes 2x unreachable.
  • Build: go run bin/cross-compile.go -compile-only for all targets, and a before/after rclone binary size comparison.

9.8 Fork CI

The fork has no CI today (upstream workflows are gated to rclone/rclone and the remote is Gitea). Add Gitea Actions workflows:

  • on push: build, go vet, golangci-lint, unit + fake-server suites with -race, short fuzz, govulncheck;
  • nightly: live test_all for protondrive with secrets, serialised to one run at a time, followed by a cleanup that empties the test account;
  • weekly: check for a new SDK tag and open a reminder to walk the conformance checklist.

10. Documentation

  • docs/content/protondrive.md: rewrite the hand-written parts - sign-in methods, third-party notice, what is verified, caching and events, limits, and removal of the stale "Beta", transcript and henrybear327 library text. Option sections stay generated.
  • docs/data/backends/protondrive.yaml: features and score updated with bin/manage_backends.py; the tier field is the maintainers' to change.
  • docs/content/changelog.md is the maintainers' at release.
  • Fork: docs/ARCHITECTURE.md and docs/DECISIONS.md updated in step.

11. Delivery

Milestones. Each ends with all earlier tests still green.

Milestone Outcome
M0 Scaffolding Fake server skeleton; differential test harness against the old libraries; fork CI. Golden fixtures as soon as a test account exists
M1 Session and account Both sign-in methods, refresh, key unlock, on the fake and live
M2 Read path Listing, NewObject, download with verification and ranges
M3 Write path Upload, revisions, drafts, verification tokens, PutStream
M4 Mutations and swap Mkdir, Rmdir, Move, DirMove, Copy, Purge, CleanUp, About; bridge libraries removed; hermetic and live suites green; the fork's backend is usable for real data
M5 Events In-memory cache invalidation, ChangeNotify, Shutdown
M6 Persistent cache and metadata metadata_cache, Metadata feature, small-file upload path, bisync and gitannex green
M7 New crypto model When Proton publishes it in the SDK

The fork branch replaces the old implementation in place from M1 onward - master and released rclone remain the reference for the old behaviour - so there is never a second package or a selector. M1 to M3 are verified on the fake and, read-only, live; the branch is not usable for real data until M4. Upstream receives it as one commit series at M4, as rclone did for the S3 SDK v2 and Azure SDK migrations. (call)

The steps only a person can do - creating the test account, configuring it as the TestProtonDrive: remote in their own rclone config (credentials are never pasted into an agent conversation or committed), and the first interactive login for fixture capture and interop - are the fork owner's. Everything that needs the live service (9.7, criteria 3, 6 and 7) waits for that and is reported as not done until it has run.

Update 2026-11-11: the owner created two test accounts, a free one (TestProtonDrive:) and a paid one (TestProtonDrivePaid:), and supplied them configured with username and password on the development machine. Early live probes against the free account have run (handoff section 6); the formal live verification (9.7, plan Task 21) has not.

12. Acceptance criteria

The first upstream milestone (M4) is done when:

  1. go.mod no longer contains rclone/Proton-API-Bridge or rclone/go-proton-api, has no replace, and the release binary is not larger than before.
  2. All targets cross-compile without cgo.
  3. A config file written by rclone v1.76 works unchanged, without re-login.
  4. The fake server never sees a legacy route, a missing verifier, a spoofed app version or an SDK-version header.
  5. Hermetic fstests is green with -race, in fork CI so are fs/operations, fs/sync and vfs, and every test in 9.4 and 9.6 not marked M5 or M6 passes.
  6. A live test_all run for protondrive is clean with no ignore: entries.
  7. Interop passes in both directions with the official CLI and the web app.
  8. Features: PutStream, Copy, Move, DirMove, Purge, About, CleanUp, ListP, multi-thread download; SHA1; feature score at least 5.
  9. The docs page is current, and carries the third-party notice.
  10. Each of these issues has a passing regression test or a recorded live verification: #10224, #9622, #9880, #9816, #9832, #9132, #8993/#9207, #7959, #9438.

The spec as a whole is done at M6, plus: ChangeNotify works under mount; request budgets hold; bisync and gitannex run green and are no longer excluded; Metadata gives 6/7.

What this cannot deliver by itself. Two Tier 1 attributes are not code: "Maintainers >= 2" and "API source: Official". The tier label is the rclone maintainers' decision. A realistic outcome is Tier 2 on merge, with Tier 1 depending on a second maintainer and on Proton's SDK reaching general availability for third parties. Even Tier 2 lists "API source: Official", so it relies on the discretion tiers.md allows; if the maintainers read a Go port of the SDK as unofficial, the ceiling is Tier 3 regardless of quality.

13. Upstream path

Deferred by the owner's decision. Kept as the route to take if and when the fork's backend is offered upstream.

  1. Before M1, ask. CONTRIBUTING requires big features to be discussed first. Post a summary of sections 1-6 and 9 on #9252 and in forum thread 53609 (where ncw and Proton's engineers talk), and ask ncw the questions in section 14. Nothing here has been posted yet.
  2. Offer maintainership. ncw has asked for a Proton Drive maintainer since October 2025. The offer, a test account for the integration server and a CODEOWNERS entry are part of the proposal, not an afterthought.
  3. Credit and coordinate with authors of overlapping open PRs: #9522 (browser login), #9881 (refresh race), #9851 (name-hash fallback), #9834 (bandwidth), #9081 (verification tokens).
  4. The upstream branch is cut from master and contains only backend/protondrive/, go.mod, go.sum, docs/content/protondrive.md, docs/data/backends/protondrive.yaml and fstest/test_all/config.yaml changes - none of the fork-only paths in AGENTS.md.
  5. Tell Proton. The same forum thread is where to state the app version, the routes used and the event polling interval, and to ask when the block-verification exemption can be dropped for the new version.

14. Open questions

Answered by the fork owner on 2026-11-11:

  1. Maintainership: no permanent maintenance is planned, so no offer is made upstream. Tier 1's "Maintainers >= 2" is therefore out of reach through this fork; the code-level attributes remain the target.
  2. Test accounts: a free Proton account will be created for testing, possibly a new member of the owner's Proton family plan. The live matrix in 9.7 starts as "free account" only; the other account types are recorded as untested.
  3. Upstream post: not now. Build something working first.
  4. Fake server: accepted at roughly a third of the code volume, kept in the fork and not offered upstream.

Added 2026-11-11: a paid account was also provided, so the live matrix in 9.7 is free plus paid. The paid account has not been signed in to yet.

Still open, for whenever the upstream path is taken.

For rclone's maintainers (to ask in step 13.1):

  1. In-place replacement under the same backend name, or a period with both implementations selectable?
  2. Is an in-package fake server and a hermetic fstests run welcome upstream, or should it stay in the fork?
  3. Is a persistent on-disk metadata cache acceptable in a backend, given it stores ciphertext only?
  4. Is browser sign-in acceptable as the default for new remotes?
  5. Is a hidden endpoint option for tests acceptable?

For Proton (to ask in step 13.5):

  1. Which channel should development builds send, given the grammar has no dev?

15. Risks

Risk Response
Proton enforces "official SDK only" through the requirements headers or app-version blocks Conformance work, honest identification and the open channel with Proton's engineers make this unlikely; the sidecar design is the documented fallback
The new crypto model lands before M4 Crypto is isolated; M7 is planned; old-format reads stay. Timeline risk is real and outside our control
A port that is wrong in the same way as its fake Golden fixtures from official clients, SDK-derived vectors, and two-way live interop
ncw's review bandwidth Small commits, hermetic tests he can run, and an offered maintainer
SDK drift (0.x, breaking minors, roughly fortnightly releases) Pinned reference tag, conformance checklist, weekly reminder
SRP password login is withdrawn for third-party app versions Browser sign-in is already the default path
Proton's README says the SDK is "not yet ready for third-party production use" The backend stays honestly labelled in the docs; Proton has said it "does not prohibit the usage of rclone"

16. Sources

Researched 2026-11-11: rclone #9252, #8873, #8870, #9081, #9189, #9260, #9522, #9622, #9816, #9832, #9844, #9880, #9881, #10224; forum threads 53609 and 52742; ProtonDriveApps/sdk at 05c9115 (README, client/js, incubating/account, cli), its PR #19 and issues #5, #6, #7, #11, #17, #21, #22, #35, #72, #74; docs/content/tiers.md, CONTRIBUTING.md, fstest/fstests/fstests.go and backend/protondrive/ in this tree at 448d7361c.