fix(encryption): decrypt versions and trashbin so encryption can be disabled#41624
Conversation
…isabled occ encryption:decrypt-all only walked the regular "files" folder, leaving the "encrypted" flag set on file cache entries under "files_versions" and "files_trashbin". Since occ encryption:disable refuses while any file cache row is still flagged as encrypted, administrators could not disable encryption even though decrypt-all reported success. decrypt-all now also descends into "files_versions" and "files_trashbin" when they exist, and the disable command now lists the paths that are still flagged as encrypted along with a remediation hint instead of printing a generic message. Fixes #41623 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
33ca698 to
dbe5ea1
Compare
jvillafanez
left a comment
There was a problem hiding this comment.
it makes sense, just nitpicking.
Address review feedback: instead of an arbitrary 50-path cap, print at most 20 still-encrypted paths so the output fits on screen, followed by an exact "... and N more still encrypted" summary. The count is determined with a dedicated COUNT(*) query so the full file cache is never loaded into memory. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
DeepDiver1975
left a comment
There was a problem hiding this comment.
Code Review — fix(encryption): decrypt versions and trashbin so encryption can be disabled
🤖 Automated review by Claude Code review agent.
Overview
Fixes the scope mismatch where encryption:decrypt-all only walked /<uid>/files while encryption:disable refuses if any filecache row has encrypted >= 1. The fix makes decrypt-all also descend into files_versions and files_trashbin, and improves the disable command's diagnostics by listing the still-encrypted paths. The root-cause analysis in the description is accurate and matches the code.
Strengths
- Correct root cause & minimal fix. Adding the two extra roots in
decryptUsersFiles()is the right place — the existing BFS loop, share-skipping, anddecryptFile()flag-clearing all apply unchanged to the new roots. - Defensive
is_dir()guard avoids seeding non-existent roots into the walk (versions/trashbin are absent on many setups). - Much better operator UX. Listing blocking paths plus the remediation hint about shared/external storage (which decrypt-all skips by design) directly addresses the "decrypt-all said success but disable still fails" confusion.
- Test coverage is solid —
testDecryptUsersFilesAlsoCoversVersionsAndTrashbinasserts exactly the three roots are visited, andDisableTestnow exercises the capped-path + "... and N more" summary path.
Issues / Suggestions
-
Two queries where one would do (minor, performance).
Disable::execute()now runs aCOUNT(*)and then a secondSELECT ... LIMIT 20. You could drop the count query and derive the "... and N more" suffix only when the path query hits the cap — though knowing the exact remaining count requires the count anyway. As-is it's fine; just noting the extra round-trip runs on the common (zero-encrypted) success path too. Consider short-circuiting: if the path-list query returns 0 rows, skip straight to disabling. Currently the success path pays for aCOUNT(*)scan that could be a cheapLIMIT 1existence check when you don't need the number. Trade-off is acceptable given disable is a rare admin command. -
createFunction('COUNT(*)')portability. UsingcreateFunctionfor the count bypasses the query builder's aggregate handling. It works across OC's supported DBs here, but$qb->func()->count('*')(orselectAlias) is the more idiomatic and safer route. Low priority. -
getDirectoryContentonfiles_trashbinroot. Trashbin layout isfiles_trashbin/files/...plusversions/andkeys/siblings. The BFS will recurse into all of them and attemptdecryptFile()on non-file artifacts.decryptFile()returning false is handled gracefully (counted as "already decrypted"), so this is safe, but worth a manual check that decrypting trashbinversions/entries actually clears theirencryptedflag — otherwise the disable block could persist for those rows. The PR's own "Note for QA" already flags this; +1, please verify against a live per-user-key instance. -
Test seeding contract change.
testDecryptUsersFilesnow also returns true for/user1/files(previously only/user1/files/foo). Correct given the newis_dirguard, but confirm no other test relied on the old narrower stub.
Risk Assessment
Low-to-moderate. Logic change is contained and well-tested at the unit level. The main residual risk is the real-world behavior of decryption on trashbin/versions storage backends (master-key vs per-user-key), which unit tests can't cover — the manual QA note correctly calls this out.
Verdict
👍 Approve with minor suggestions. The fix is correct, minimal, and well-tested. Recommend addressing the live-instance QA verification before release per the author's own note.
Summary
Fixes #41623.
occ encryption:decrypt-allreports success butocc encryption:disablestill fails with "The system still has encrypted files. Please decrypt them all before disabling encryption." — leaving admins unable to turn encryption off (also see the central thread linked in the issue, and the closely related #39212).Root cause is a scope mismatch between the two commands:
encryption:disable(core/Command/Encryption/Disable.php) refuses if anyfilecacherow hasencrypted >= 1.decrypt-all(lib/private/Encryption/DecryptAll::decryptUsersFiles()) only ever walks'/' . $uid . '/files', anddecryptFile()is the only place that resets the flag.So encrypted entries under
files_versionsandfiles_trashbinkeep their flag and permanently block disabling, even though decrypt-all printed "all files could be decrypted successfully!".Changes
DecryptAll:decryptUsersFiles()now also descends intofiles_versionsandfiles_trashbin(when they exist), so their entries are actually decrypted and theirencryptedflag cleared.Disable: instead of a generic message, the command now lists the paths still flagged as encrypted (capped at 50) plus a remediation hint, so admins can see what is blocking them — e.g. entries on shared/external storage, which decrypt-all skips by design and must be decrypted by their owner.Tests
DisableTestfor the newfetchFirstColumn()query and multi-line output; data provider now drives blocking-path scenarios and asserts the offending paths are printed.DecryptAllTest::testDecryptUsersFilesfor the new seeding contract and addedtestDecryptUsersFilesAlsoCoversVersionsAndTrashbinproving all three roots are walked when present.DisableTest(6 tests) andDecryptAllTest(24 tests) pass locally on sqlite; php-cs-fixer clean.Note for QA
Unit tests prove the new roots are visited; end-to-end decryption of versions/trashbin against a live encrypted instance (master-key and per-user-key modules) should be verified manually before release.
🤖 Generated with Claude Code