Files
rclone/docs/plans/001-protondrive-tier1-workspace/task-21g-review.md
T
2026-11-12 15:12:51 +01:00

10 KiB

Task 21g review (502a32b5f..a2dc6f71f)

Line numbers are of backend/protondrive/golden_internal_test.go at a2dc6f71f, not of the diff.

Spec Compliance

  • ❌ Issues found: sanitisation fails open in two places, which goes against the owner's rule that no account secret or ID reaches a fixture:

    • golden_internal_test.go:1258: an intended-recipient fingerprint that is not in the replacement map is written in hex into the fixture, inside Rekeyed in nodes.json (I-1).
    • golden_internal_test.go:898-908: the user-ID name of a node key is copied as it is, and an email that doesn't belong to the account is kept (I-2).

    Everything else in the brief is done. Two deviations are justified and documented in the report and in DECISIONS.md:

    • Embedded signatures force the SEIPD packets of names, hash keys and xattrs to be encrypted again, with the same session key and parameters.
    • Node passphrases are replaced.

    Also documented: block signatures are dropped and no user key is written.

  • ⚠️ Cannot verify from the diff: how the code behaves on real web-app and CLI data. That covers v6 signatures, text signatures, intended recipients, SignerUserId, RSA keys and several PKESKs; the fake produces none of them. The controller must run inspection steps 2, 4 and 6 of the report on the live output, and also look at the decrypted xattrs (see M-3).

Checks run

  • Leak, packet level. I ran a throwaway copy of the package (git archive into the scratchpad, with RCLONE_CONFIG=/notfound) that captures the round-trip seeds and parses every armored blob and ContentKeyPacket in the fixture. Every PKESK key ID, signature issuer key ID and issuer fingerprint belongs to a key in keys.json or nodes.json. None belongs to the source address, root or node keys, and no source key ID or fingerprint appears in nodes.json as text. The test PASSED. The fake writes no intended recipients and no SignerUserId, so those two branches were not exercised.
  • Re-made signatures. resignSignature (golden_internal_test.go:1232-1283) builds each signature from scratch. It copies only the type, hash, creation time, lifetime and notations. The issuer key ID is the new key's, and go-crypto Signature.Sign sets IssuerFingerprint from the signing key (go-crypto@v1.5.2 signature.go:978). Unhashed subpackets, embedded signatures and the original salt are not carried over.
  • PKESKs. wrap (golden_internal_test.go:926-941) makes one new PKESK to the test key. All original key packets are dropped (golden_internal_test.go:959-1007).
  • NodePassphrase. It is replaced, including inside a compressed packet, because plain recurses (golden_internal_test.go:1093). Its session key is new (golden_internal_test.go:1008-1015).
  • Unexpected inner packets. Any packet that is not compressed, literal, one-pass signature or signature fails the capture (golden_internal_test.go:1131-1132), so no stray key or user-ID packet can pass through.
  • Emails. owns is case-insensitive (account.go:261). An email not of the account fails the capture (golden_internal_test.go:893).
  • IDs, URLs, tokens. Only link-N placeholders are written. Volume, share, revision, block and address IDs, URLs and tokens are never put in the structs that get written (golden_internal_test.go:616-621, 722-750).
  • User key. No user key is written.
  • Terminal output. It holds node names, notes, and in error messages foreign emails or unknown key IDs. No passphrases, tokens or keys are printed.
  • Read-only. I checked the methods used: RootID(ctx, false), getNode, listChildren, open, revisionBlocks and downloadBlock are all reads. Fs.Shutdown only clears the cache (protondrive.go:1066-1069); it does not log out.
    • Files are written only after the whole capture succeeds (golden_internal_test.go:474), and an existing directory is refused (golden_internal_test.go:429-431).
    • A capture with ignore_signature_errors on is refused (golden_internal_test.go:433-435).
    • NewFs would create a volume on an account that has none (nodes.go:410). That is normal backend behaviour and does not apply to an account the web app has used.
  • protontest changes. insertLink's callers (drive.go:555, 719) never set modifyTime, so keeping a given modification time changes nothing for them.

Strengths

  • The re-keying is careful and close to the original format:
    • same PKESK version, SEIPD version, AEAD mode, chunk size and compression algorithm;
    • the literal packet kept verbatim;
    • SEIPD packets with no signature kept byte for byte.
  • Every node records what was done to it in Rekeyed.
  • Strict refusals: unverified nodes, any problem the backend reported, unknown signers, foreign emails, thumbnails, unsupported key algorithms.
  • checkGolden is a meaningful test:
    • it reads only through Fs/Object, with ignore_signature_errors=false;
    • it compares listings;
    • per file it checks size, modification time, MIME type, Hash and a full read against SHA-1;
    • the ranged read starts halfway into block 2 (golden_internal_test.go:412-421);
    • it fails if anything was reported or skipped (golden_internal_test.go:376-379).
  • The capture loads and checks its own output straight away (golden_internal_test.go:143-144).
  • The round trip covers SDK-style files, files with unrecorded block sizes, legacy files, anonymous files and the file cap. It checks the blocks are byte-identical, and its secret list is broad (golden_internal_test.go:266-288).

Issues

Critical (Must Fix)

None.

Important (Should Fix)

  • I-1, golden_internal_test.go:1256-1259: leak. When a signature's intended recipient is not in rk.fingerprints, the note intended recipient %X, an unknown key, left out writes the original fingerprint into nodes.json Rekeyed. That is a public identifier of a real account key: the account's user key, another member's key, or a subkey the map doesn't cover. It ties the fixture to the account, and the brief says every key ID is replaced. Fix: fail the capture, or write the note without the fingerprint. Add the hex and base64 fingerprints of the source keys to the round trip's secret list.
  • I-2, golden_internal_test.go:898-908: leak path. identity passes only the email part of a node key's user ID through owns. The name part is copied as it is, and a non-account email is kept. Proton address-style user IDs put the address in the name as well (addr@proton.me <addr@proton.me>), and a client could put a display name or another address there. The result sits inside armored keys, where the report's email grep (step 2) can't see it. Fix: treat the name with the same rule (use nodeKeyUserID when the name contains @ or doesn't match the original's generic value), and fail on, or drop, non-account emails, as rk.email already does.
  • I-3 (plan-mandated), golden_internal_test.go:38-39, 108-128: unsafe flag. The capture reuses -remote. If the -run pattern is wrong or missing, TestIntegration in protondrive_test.go runs the full write/delete suite against the real account. TestIntegrationFake skips in that case (fake_test.go:66), and nothing in TestGoldenCapture can stop the other test. The brief specifies -remote, but a read-only capture should not be one regex away from a writing suite. Fix: give the capture its own flag (for example -capture-remote), so that -remote is never set during a capture.

Minor (Nice to Have)

  • M-1: leak test checks strings only. The leak check in TestGoldenRoundTrip (golden_internal_test.go:266-288) searches raw and JSON-escaped strings. It does not parse packets, so a source key ID or fingerprint inside an armored signature or PKESK would not be found. Add a packet walk like the one I ran: every PKESK, issuer and recipient must belong to a test key.

  • M-1b: no automated tamper test. No test shows that checkGolden fails on a tampered fixture, for example one signature byte flipped. The mutation check exists only in the report.

  • M-2: salt notation added. At golden_internal_test.go:1248 and 1275, Sign with a nil config removes any salt@notations.openpgpjs.org notation and appends a fresh one (go-crypto signature.go:979-992). Every v4 signature that is made again therefore carries a salt notation, even when the original (for example from the Go SDK or CLI) had none. So "same notations" in DECISIONS.md and the report is not exact. Note it in Rekeyed when the original had no salt notation, or turn the salt notation off when the original had none.

  • M-3: other data the fixture still holds. The report's inspection step 5 says Want is the only plaintext. That is wrong: names, xattrs and folder hash keys can be decrypted with the committed test keys.

    • Xattrs are kept as written, so a media file from the web app could carry Camera or Location fields.
    • The literal-packet file name and date are kept (golden_internal_test.go:1114).
    • The real folders' hash keys are exposed. Anyone can then test whether a guessed name exists in those real folders.
    • The address key's creation time is kept (golden_internal_test.go:572). That is a weak account fingerprint.

    Add "decrypt and read the xattrs" to the inspection steps, and state the exposure of hash keys and session keys in the DECISIONS.md entry.

  • M-4: folder times unchecked. golden_internal_test.go:357-366 records Want.ModTime for folders but never checks it.

  • M-5: one-pass signature pairing. At golden_internal_test.go:1139-1141 and 1156-1169, the nesting assumes every signature has a one-pass signature. A message mixing a prefixed signature with one-pass signatures would pair them wrongly. The read-back after capture would catch it, but the error would be confusing.

Assessment

Task quality: Needs fixes

Reasoning: The re-keying is careful, and my packet-level check of the fake round trip found no source key IDs, fingerprints, emails or passphrases. Two paths still copy data from the real account into the fixture (an unmapped intended-recipient fingerprint in the notes, and a node key's user-ID name or a non-account email). Reusing -remote leaves a writing suite one -run slip away. All three should be fixed before the live capture.