Conversation
… block size fixUnencryptedSize() asked the encryption module for the unencrypted block size before calling begin(). The module only learns a file's encoding (binary or legacy base64) from the header in begin(), so it answered from its default and returned the binary block size for every file. For legacy base64 files that is 8096 instead of 6072 bytes per block, and the recalculated unencrypted_size ends up exactly 4/3 too large. The wrong value breaks downloads (Content-Length larger than the data), shows files as 0 B when the recalculation cannot run, and is never corrected again because it stays below the on-disk size and so passes every plausibility check in verifyUnencryptedSize(). Since the position suffix "end" for the last block is derived from unencrypted_size, the inflated value also makes the last block fail its signature check. begin() only resolves file key, cipher, version and encoding and does not touch the stream, so it can be called before the block size is read. Fixes nextcloud#63662 Signed-off-by: Maximilian von Heyden <maximilian@vheyden.de> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
glazperle
requested review from
Altahrim,
icewind1991,
leftybournes and
salmart-dev
and removed request for
a team
September 19, 2026 08:35
Author
|
/backport to stable34 |
Author
|
/backport to stable33 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
fixUnencryptedSize()inlib/private/Files/Storage/Wrapper/Encryption.phpasked the encryption module for the unencrypted block size before callingbegin(). The module only learns a file's encoding (binary or legacy base64) from the header insidebegin(), sogetUnencryptedBlockSize()always answered from its default$useLegacyBase64Encoding = false(apps/encryption/lib/Crypto/Encryption.php:53) and returned the binary block size for every file.For legacy base64 files that is 8096 instead of 6072 bytes per block, and the recalculated
unencrypted_sizeends up exactly 4/3 too large.This PR moves the
begin()call in front of the block size lookup.begin()only resolves file key, cipher, version and encoding; it does not touch the stream, so the reordering has no other effect.Fixes #63662
Why this matters
Content-Lengthlarger than the data and break; the desktop client reportsRemoteHostClosedError, curl(18) end of response with X bytes missing.unencrypted_size = 0and show as 0 B. The first access from a logged-in session then writes the inflated value.verifyUnencryptedSize(). It is never corrected again.readCache()inlib/private/Files/Stream/Encryption.phpderives theendsuffix of the last block's HMAC position fromunencrypted_size. With an inflated value the real last block is verified asNinstead ofNendand fails with "Bad Signature" — see [Bug]: Encryption: AES-256-CTR legacy files fail signature verification on final block - Bad Signature #64078.The wrong calculation is old, but it only started being persisted with eca9503 (#62780 for 34.0.3, #62779 for 33.0.8), which added recalculation for rows at zero. Before that the value was computed wrongly but never written.
Test
testFixUnencryptedSizeUsesBlockSizeFromHeaderbuilds a module mock that answers 8096 beforebegin()and 6072 after it — the way the real module behaves — feedsfixUnencryptedSize()a stream with a header, three full blocks and a partial last block, and expects3 · 6072 + strlen(plaintext of the last block).There was no test exercising
fixUnencryptedSize()itself so far;testVerifyUnencryptedSizemocks it away.24388(= 3 · 8096 + 100) against expected18316.EncryptionTest.phpis unchanged (the DB-access failures in that file when run outside--group DBare pre-existing and identical with and without the patch).Reproducing on a fresh instance
No legacy data needed:
'encryption.use_legacy_base64_encoding' => trueinconfig.php, upload a file of a few hundred KB.UPDATE oc_filecache SET unencrypted_size = 0 WHERE fileid = <id>;unencrypted_sizeis rewritten to 4/3 of the plaintext size and the download is cut short.Repairing existing rows
Not part of this PR. The plaintext size can be recomputed from the file's block structure without decryption or keys; scripts and details are in #63662. A follow-up as an
occcommand is possible if wanted.Checklist
🤖 Generated with Claude Code