Wrong unencrypted_size for legacy base64 files: block size is read before the file header is applied
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 36.9k
- Forks
- 5.2k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 713
Description
fixUnencryptedSize() in lib/private/Files/Storage/Wrapper/Encryption.php calls getUnencryptedBlockSize() before it calls begin(). A file's encoding (binary or legacy base64) is only known once begin() has parsed the header, so the block size comes from the module default instead of from the file.
For files still stored in base64 that gives 8096 instead of 6072 bytes per block. The recalculated size ends up 4/3 too large, Content-Length is wrong, and the download breaks.
Code
apps/encryption/lib/Crypto/Encryption.php:
private bool $useLegacyBase64Encoding = false;
public function getUnencryptedBlockSize($signed = false) {
if ($this->useLegacyBase64Encoding) {
return $signed ? 6072 : 6126;
}
return $signed ? 8096 : 8168;
}
The flag is set in begin():
$this->useLegacyBase64Encoding = true;
if (isset($header['encoding'])) {
$this->useLegacyBase64Encoding = $header['encoding'] !== Crypt::BINARY_ENCODING_FORMAT;
}
lib/private/Files/Storage/Wrapper/Encryption.php, in fixUnencryptedSize():
$signed = isset($header['signed']) && $header['signed'] === 'true';
$unencryptedBlockSize = $encryptionModule->getUnencryptedBlockSize($signed);
// ... last chunk is read ...
$encryptionModule->begin($this->getFullPath($path), $this->uid, 'r', $header, []);
The module is constructed fresh on every call, so the value is always the default and not something left over from another file. Same order in master.
Steps to reproduce
- Server-side encryption, with files written before binary encoding became the default (header has
signed:trueand noencoding:binary). - Trigger a recalculation, for example with an
unencrypted_sizeof 0 inoc_filecache.verifyUnencryptedSize()also triggers on a negative value, on a value equal to the on-disk size, and on a value larger than it. unencrypted_sizeis stored 4/3 too large.- The download announces more Content-Length than there is data.
Expected
The block size follows the encoding recorded in the file's own header.
Fix
Call begin() before getUnencryptedBlockSize(). begin() does not touch the stream, it only resolves file key, cipher, version and encoding.
Notes from a live instance
34.0.3, per-user keys, 313775 encrypted files, 81277 of them base64. I recalculated the plaintext sizes from the block structure on disk, independently of Nextcloud. Every affected file was off by exactly 4/3, for example 383550 stored against 288422 real.
With per-user keys fixUnencryptedSize() cannot decrypt from cron at all and only logs Couldn't re-calculate unencrypted size for .... Those files stay at unencrypted_size = 0 and show as 0 B. The first access from a logged-in session then replaces the 0 with the inflated value instead of the correct one.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/private/Files/Storage/Wrapper/Encryption.php at fixUnencryptedSize(), then read begin() and getUnencryptedBlockSize() in apps/encryption/lib/Crypto/Encryption.php. Verify the header is applied before the block size is read, and confirm legacy base64 files use the recorded encoding so recalculated unencrypted_size and Content-Length match the actual data.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100