client: range-block overlap helper; document pre-offset skip blocker
ober
7e3f0969635d6c711131e8c857d9d7ce90d283e8
--- a/protonstorage/drive/client.ss +++ b/protonstorage/drive/client.ss @@ -26,6 +26,7 @@ proton-drive-active-file-revision proton-drive-read-file-bytes proton-drive-read-file-range + proton-drive-overlapping-blocks proton-drive-create-folder-at-path proton-drive-create-root-folder proton-drive-upload-file-at-path-bytes @@ -397,6 +398,39 @@ (download-decrypt-block session content-key block file-key address-key)) blocks)))) + ;; Revision blocks carry only their ENCRYPTED size ("Size"); the per-block + ;; PLAINTEXT size is not published in the block list. The only authenticated + ;; source of plaintext block sizes is the revision XAttr "Common.BlockSizes" + ;; (see drive/write.ss), which is encrypted+signed and which the read path + ;; does not decrypt: no RustSec-clean Proton OpenPGP backend exists yet (see + ;; SECURITY.md and the disabled native crypto in drive/crypto.ss). The + ;; manifest signature covers only the concatenated encrypted block hashes, not + ;; sizes. A fixed block size (proton-drive-default-upload-block-size) cannot + ;; be assumed either: the upload block size is configurable and differs for + ;; files written by other Proton clients, so guessing boundaries could skip + ;; the wrong blocks and return corrupted plaintext. Until authenticated + ;; plaintext block sizes are available on the read path, pre-offset blocks + ;; cannot be skipped safely; return #f to select the sequential + ;; decrypt-from-start path in proton-drive-read-file-range. + (def (revision-plaintext-block-sizes revision) + #f) + + ;; Given authenticated plaintext block sizes (in block order) and a byte + ;; offset `start`, drop the leading blocks whose plaintext range ends at or + ;; before `start`. Returns (values remaining-blocks head-offset) where + ;; head-offset is the plaintext byte offset of the first remaining block, so a + ;; ranged read can decrypt only the blocks overlapping the range instead of + ;; every block from the start. + (def (proton-drive-overlapping-blocks blocks plain-sizes start) + (let loop ([bs blocks] [sizes plain-sizes] [pos 0]) + (cond + [(or (null? bs) (null? sizes)) (values bs pos)] + [else + (let ([block-end (+ pos (car sizes))]) + (if (<= block-end start) + (loop (cdr bs) (cdr sizes) block-end) + (values bs pos)))]))) + (def (proton-drive-read-file-range session share-id parent-key file-link size offset address-key) (unless (proton-drive-link-file? file-link) @@ -412,33 +446,43 @@ (let* ([total (link-size file-link)] [start (if (and (number? offset) (> offset 0)) offset 0)] [count (if (and (number? size) (>= size 0)) size total)] - [end (min (+ start count) total)]) - ;; Revision blocks carry no plaintext size, so blocks before `start` - ;; cannot be skipped; decrypt in index order but stop once past `end`, - ;; keeping only the portion overlapping [start,end). + [end (min (+ start count) total)] + [plain-sizes (revision-plaintext-block-sizes revision)]) + ;; When authenticated plaintext block sizes are available, drop the + ;; blocks ending at or before `start` so they are never downloaded or + ;; decrypted (proton-drive-overlapping-blocks). Until then plain-sizes is + ;; #f (see revision-plaintext-block-sizes) and blocks must be decrypted + ;; from the start to learn each plaintext length, stopping once past + ;; `end` and keeping only the portion overlapping [start,end). (if (>= start end) (make-bytevector 0) - (let loop ([bs blocks] [pos 0] [acc '()]) - (cond - [(or (null? bs) (>= pos end)) - (concat-bytevectors (reverse acc))] - [else - (let* ([block (car bs)] - [plain - (download-decrypt-block - session content-key block file-key address-key)] - [blen (bytevector-length plain)] - [block-end (+ pos blen)]) - (if (<= block-end start) - (loop (cdr bs) block-end acc) - (let* ([sel-start (max 0 (- start pos))] - [sel-end (min blen (- end pos))] - [sel (bv-slice plain sel-start sel-end)]) - (loop (cdr bs) - block-end - (if (> (bytevector-length sel) 0) - (cons sel acc) - acc)))))])))))) + (call-with-values + (lambda () + (if plain-sizes + (proton-drive-overlapping-blocks blocks plain-sizes start) + (values blocks 0))) + (lambda (bs pos) + (let loop ([bs bs] [pos pos] [acc '()]) + (cond + [(or (null? bs) (>= pos end)) + (concat-bytevectors (reverse acc))] + [else + (let* ([block (car bs)] + [plain + (download-decrypt-block + session content-key block file-key address-key)] + [blen (bytevector-length plain)] + [block-end (+ pos blen)]) + (if (<= block-end start) + (loop (cdr bs) block-end acc) + (let* ([sel-start (max 0 (- start pos))] + [sel-end (min blen (- end pos))] + [sel (bv-slice plain sel-start sel-end)]) + (loop (cdr bs) + block-end + (if (> (bytevector-length sel) 0) + (cons sel acc) + acc)))))])))))))) (def (link-state link) (let ([value (jmaybe link "State" 0)]) --- a/test/test-all.ss +++ b/test/test-all.ss @@ -970,6 +970,37 @@ (lambda () (proton-drive-current-manifest-signature-verifier old-verifier))))) +(check "ranged read skips pre-offset blocks when plaintext block sizes are known" + (let* ([mk-block + (lambda (index) + (let ([b (make-hashtable equal-hash equal?)]) + (hashtable-set! b "Index" index) + b))] + [blocks + (list (mk-block 1) (mk-block 2) (mk-block 3) (mk-block 4))] + [sizes '(10 10 10 5)]) + (and + (call-with-values + (lambda () (proton-drive-overlapping-blocks blocks sizes 0)) + (lambda (remaining head-offset) + (and (= head-offset 0) (= (length remaining) 4)))) + (call-with-values + (lambda () (proton-drive-overlapping-blocks blocks sizes 12)) + (lambda (remaining head-offset) + (and (= head-offset 10) + (= (length remaining) 3) + (= (hashtable-ref (car remaining) "Index" 0) 2)))) + (call-with-values + (lambda () (proton-drive-overlapping-blocks blocks sizes 30)) + (lambda (remaining head-offset) + (and (= head-offset 30) + (= (length remaining) 1) + (= (hashtable-ref (car remaining) "Index" 0) 4)))) + (call-with-values + (lambda () (proton-drive-overlapping-blocks blocks sizes 100)) + (lambda (remaining head-offset) + (and (= head-offset 35) (null? remaining))))))) + (check "content session key decrypt verifies packet signature before decrypting" (let* ([file-properties (make-hashtable equal-hash equal?)] [file-link (make-hashtable equal-hash equal?)]