From bdfa908ffd7fdb3b6f092c7b4ec03df3791adf76 Mon Sep 17 00:00:00 2001 From: Josh Miller Date: Wed, 25 Feb 2026 08:15:02 -0500 Subject: [PATCH] drive: fix two upload failures caused by Proton backend changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes two bugs that prevent file uploads to Proton Drive: --- Bug 1: handleRevisionConflict ignores ReplaceExistingDraft when GetRevisions returns 2501 --- When a previous upload attempt fails after the file draft is created but before blocks are committed, a broken draft remains. On the next attempt the draft is found (2500 "file already exists"), and GetRevisions is called to decide what to do with it. However, broken drafts return 422/2501 from the /revisions endpoint, causing handleRevisionConflict to return an error immediately — even when ReplaceExistingDraft=true. Fix: if GetRevisions fails AND ReplaceExistingDraft is set AND the link is in draft state, treat it as a broken draft and delete the link so the caller can retry from scratch. --- Bug 2: block uploads missing required Verifier.Token --- Proton's storage backend now requires a Verifier.Token per block in the POST /drive/blocks request. Without it, the storage server rejects block uploads with HTTP 422 / Code=200501 "Operation failed: Please retry". The token is computed by fetching a VerificationCode for the revision via: GET /drive/v2/volumes/{volumeID}/links/{linkID}/revisions/{revisionID}/verification then XOR-ing it with the leading bytes of each block's ciphertext (algorithm sourced from the official Proton Drive JS SDK in ProtonDriveApps/sdk). Also bumps go-proton-api to v1.0.1-0.20260218123427-1a63a293e3a2 which updates the default API host from mail.proton.me/api to drive-api.proton.me. Note: the JS SDK performs an additional client-side decryption check before computing the XOR token (to detect bit-flips / bad hardware). That step is not implemented here; the server-side manifest signature still provides end-to-end integrity verification. A future improvement could add it. This fix was identified and generated with Claude Code (AI assistant) by a non-programmer user (GitHub: lmwashere). It has not been independently reviewed by a Go or cryptography expert. Expert review before merging is strongly recommended. Reproducer: rclone copy proton: --verbose Expected: upload succeeds Actual: 422 POST fra-storage.proton.me/storage/blocks (Code=200501) followed by 422 GET .../revisions (Code=2501) on retry --- file_upload.go | 42 ++++++++++++++++++++++++++++++++++++++++-- go.mod | 2 +- go.sum | 2 ++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/file_upload.go b/file_upload.go index 4318df8..cd8cc95 100644 --- a/file_upload.go +++ b/file_upload.go @@ -8,6 +8,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/hex" + "fmt" "io" "mime" "os" @@ -24,6 +25,17 @@ func (protonDrive *ProtonDrive) handleRevisionConflict(ctx context.Context, link draftRevision, err := protonDrive.GetRevisions(ctx, link, proton.RevisionStateDraft) if err != nil { + // If we can't list revisions but the link is already in draft state + // (e.g. a broken/incomplete upload from a previous failed attempt) + // and the user wants to replace existing drafts, delete the link and + // let the caller retry from scratch rather than failing outright. + if protonDrive.Config.ReplaceExistingDraft && link.State == proton.LinkStateDraft { + err = protonDrive.c.DeleteChildren(ctx, protonDrive.MainShare.ShareID, link.ParentLinkID, linkID) + if err != nil { + return "", false, err + } + return "", true, nil + } return "", false, err } @@ -250,6 +262,18 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n return nil, 0, nil, "", ErrMissingInputUploadAndCollectBlockData } + // Fetch the per-revision verification code required by Proton's storage backend. + // Each block's Verifier.Token is produced by XOR-ing this code with the first + // bytes of that block's ciphertext (per the Proton Drive JS SDK spec). + revVerification, err := protonDrive.c.GetRevisionVerification(ctx, protonDrive.MainShare.VolumeID, linkID, revisionID) + if err != nil { + return nil, 0, nil, "", fmt.Errorf("uploadAndCollectBlockData: get revision verification: %w", err) + } + verificationCode, err := base64.StdEncoding.DecodeString(revVerification.VerificationCode) + if err != nil { + return nil, 0, nil, "", fmt.Errorf("uploadAndCollectBlockData: decode verification code: %w", err) + } + totalFileSize := int64(0) pendingUploadBlocks := make([]PendingUploadBlocks, 0) @@ -309,7 +333,7 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n blockSizes := make([]int64, 0) for i := 1; shouldContinue; i++ { if (i-1) > 0 && (i-1)%UPLOAD_BATCH_BLOCK_SIZE == 0 { - err := uploadPendingBlocks() + err = uploadPendingBlocks() if err != nil { return nil, 0, nil, "", err } @@ -365,17 +389,31 @@ func (protonDrive *ProtonDrive) uploadAndCollectBlockData(ctx context.Context, n } manifestSignatureData = append(manifestSignatureData, hash...) + // Compute per-block verifier token: XOR verificationCode with the + // leading bytes of the encrypted block (zero-padded if block is shorter). + verificationToken := make([]byte, len(verificationCode)) + for j, v := range verificationCode { + var b byte + if j < len(encData) { + b = encData[j] + } + verificationToken[j] = v ^ b + } + pendingUploadBlocks = append(pendingUploadBlocks, PendingUploadBlocks{ blockUploadInfo: proton.BlockUploadInfo{ Index: i, // iOS drive: BE starts with 1 Size: int64(len(encData)), EncSignature: encSignatureStr, Hash: base64Hash, + Verifier: proton.BlockUploadVerifier{ + Token: base64.StdEncoding.EncodeToString(verificationToken), + }, }, encData: encData, }) } - err := uploadPendingBlocks() + err = uploadPendingBlocks() if err != nil { return nil, 0, nil, "", err } diff --git a/go.mod b/go.mod index 62ef8b4..2f60fce 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.23.5 require ( github.com/ProtonMail/gluon v0.17.1-0.20230724134000-308be39be96e github.com/ProtonMail/gopenpgp/v2 v2.8.2 - github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18 + github.com/rclone/go-proton-api v1.0.1-0.20260218123427-1a63a293e3a2 github.com/relvacode/iso8601 v1.6.0 golang.org/x/sync v0.10.0 ) diff --git a/go.sum b/go.sum index 5b8df5d..125b082 100644 --- a/go.sum +++ b/go.sum @@ -77,6 +77,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18 h1:Lc+d3ISfQaMJKWZOE7z4ZSY4RVmdzbn1B0IM8xN18qM= github.com/rclone/go-proton-api v1.0.1-0.20260127173028-eb465cac3b18/go.mod h1:LB2kCEaZMzNn3ocdz+qYfxXmuLxxN0ka62KJd2x53Bc= +github.com/rclone/go-proton-api v1.0.1-0.20260218123427-1a63a293e3a2 h1:QR87vlRq+z0JwJsUteEhsXcSrXGJ2yte5MocMSfajM4= +github.com/rclone/go-proton-api v1.0.1-0.20260218123427-1a63a293e3a2/go.mod h1:LB2kCEaZMzNn3ocdz+qYfxXmuLxxN0ka62KJd2x53Bc= github.com/relvacode/iso8601 v1.6.0 h1:eFXUhMJN3Gz8Rcq82f9DTMW0svjtAVuIEULglM7QHTU= github.com/relvacode/iso8601 v1.6.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=