You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On a server with server-side encryption (master-key mode), the desktop client occasionally replaces a local file with the raw encrypted container from the server instead of the decrypted content, and that container is truncated. The local file becomes unusable. A conflicted copy carrying the correct content is written next to it, so the data is recoverable - but nothing indicates that the primary file is now garbage.
I have 12 specimens from 2 different days on 2 unrelated sync roots. The signature is exact and reproducible enough to be recognised, though I cannot trigger it on demand.
followed by - padding to a full 8192-byte block. For files whose plaintext is smaller than one block, that is the entire file - header block only, no ciphertext at all. For a larger one I have (plaintext 11259 bytes) the second 8192-byte block does contain high-entropy data (all 256 byte values present), so the ciphertext is there, just incomplete.
The size rule
In all 12 specimens the damaged file's size is exactly
ceil(plaintext_size / 8192) * 8192
plaintext
damaged file
complete container would be
230
8192
16384
395
8192
16384
652
8192
16384
837
8192
16384
1045
8192
16384
1109
8192
16384
1191
8192
16384
2210
8192
16384
2650
8192
16384
3518
8192
16384
4367
8192
16384
11259
16384
24576
A complete container is one header block plus ceil(plaintext / 6072) data blocks. Every specimen is exactly one 8192-byte block short of that, and the missing one is the last. That also explains the shape described above: for a plaintext below 6072 bytes the complete container is 2 blocks, one short leaves only the header block, which is why those files contain no ciphertext at all.
The written length looks like it was computed from the plaintext size and then applied to the ciphertext stream.
There is a second, independent symptom on the same instance that points at the same area. Over WebDAV, roughly 3300 files reported a getcontentlength that was too large, and downloading them aborted before the announced Content-Length with "bytes missing". The excess is an exact multiple of 2024 bytes in 1748 of 1749 files checked.
2024 is the difference between a base64-encoded block and its plaintext: base64 of 6072 bytes is 4 * ceil(6072/3) = 8096 characters, and 8096 - 6072 = 2024. The block layout is consistent with that - 8096 characters of payload plus 96 bytes of per-block overhead give the 8192-byte block.
Worth noting because it discriminates: had the index reported the container size instead of the plaintext size, the error would have been 8192 - 6072 = 2120 per block, not 2024. The measured value picks the base64 length, not the container length. Whether that shares a root cause with the truncation above I cannot say.
Impact
The local file is silently destroyed. Nothing in the client's UI marks it. Two of my cases went unnoticed for 5 days.
Two consequences that made this worse than a single lost file:
One of the damaged files was a backup script that mirrors a directory tree with rsync --delete. Had it run in that state, it would have overwritten good copies in the backup with containers and deleted what was there before.
The sync root also contained a git repository. 8 loose objects were replaced by containers, which made the repository unreadable (fatal: loose object ... is corrupt). Git objects are immutable and written concurrently, so a sync root is a bad place for one - but the corruption was not a merge conflict, the objects were literally overwritten with encryption containers.
Reproduction
I cannot trigger it deliberately. What I can say about the circumstances:
It happens together with the client writing a ... (conflicted copy <timestamp>) ... file next to the original. The conflicted copy holds the correct content; the original becomes the container.
All 12 specimens are small text files or git objects, all well under 100 KB.
2 events on 2 different days, in 2 sync roots that have nothing to do with each other.
If a conflict is a precondition, then the interesting question is why the conflict resolution path fetches the raw container rather than the decrypted stream.
Related, but not duplicates
I searched before filing:
Conflict: Server version downloaded, local copy renamed and not uploaded #1928 "Conflict: Server version downloaded, local copy renamed and not uploaded" describes the code path this happens on - the client downloads the server version and renames the local one. What I am reporting is that on an encrypted instance, what gets written is the raw container rather than the decrypted stream, and truncated at that.
nextcloud/server #54594 "Encryption logic broken" is a different defect - fclose() on a string in Storage/Wrapper/Encryption.php, hit when resuming an interrupted decrypt-all.
The header format itself is expected and documented, so the file is a well-formed SSE container, not random damage.
One thing to get out of the way early: "just turn off server-side encryption" is not a cheap workaround here. decrypt-all has several open reports of leaving files encrypted or only partially decrypted (#16597, #18709, #54594).
A note for anyone hitting this
The good content is in the conflicted copy, and for a versioned file also in the repository. A cheap detector is to check the first 24 bytes of each file for HBEGIN:oc_encryption; a full tree of 14000 files takes under a second that way.
Summary
On a server with server-side encryption (master-key mode), the desktop client occasionally replaces a local file with the raw encrypted container from the server instead of the decrypted content, and that container is truncated. The local file becomes unusable. A conflicted copy carrying the correct content is written next to it, so the data is recoverable - but nothing indicates that the primary file is now garbage.
I have 12 specimens from 2 different days on 2 unrelated sync roots. The signature is exact and reproducible enough to be recognised, though I cannot trigger it on demand.
What the damaged file looks like
The first 120 bytes are always identical:
followed by
-padding to a full 8192-byte block. For files whose plaintext is smaller than one block, that is the entire file - header block only, no ciphertext at all. For a larger one I have (plaintext 11259 bytes) the second 8192-byte block does contain high-entropy data (all 256 byte values present), so the ciphertext is there, just incomplete.The size rule
In all 12 specimens the damaged file's size is exactly
A complete container is one header block plus
ceil(plaintext / 6072)data blocks. Every specimen is exactly one 8192-byte block short of that, and the missing one is the last. That also explains the shape described above: for a plaintext below 6072 bytes the complete container is 2 blocks, one short leaves only the header block, which is why those files contain no ciphertext at all.The written length looks like it was computed from the plaintext size and then applied to the ciphertext stream.
There is a second, independent symptom on the same instance that points at the same area. Over WebDAV, roughly 3300 files reported a
getcontentlengththat was too large, and downloading them aborted before the announced Content-Length with "bytes missing". The excess is an exact multiple of 2024 bytes in 1748 of 1749 files checked.2024 is the difference between a base64-encoded block and its plaintext: base64 of 6072 bytes is
4 * ceil(6072/3)= 8096 characters, and8096 - 6072 = 2024. The block layout is consistent with that - 8096 characters of payload plus 96 bytes of per-block overhead give the 8192-byte block.Worth noting because it discriminates: had the index reported the container size instead of the plaintext size, the error would have been
8192 - 6072 = 2120per block, not 2024. The measured value picks the base64 length, not the container length. Whether that shares a root cause with the truncation above I cannot say.Impact
The local file is silently destroyed. Nothing in the client's UI marks it. Two of my cases went unnoticed for 5 days.
Two consequences that made this worse than a single lost file:
rsync --delete. Had it run in that state, it would have overwritten good copies in the backup with containers and deleted what was there before.fatal: loose object ... is corrupt). Git objects are immutable and written concurrently, so a sync root is a bad place for one - but the corruption was not a merge conflict, the objects were literally overwritten with encryption containers.Reproduction
I cannot trigger it deliberately. What I can say about the circumstances:
... (conflicted copy <timestamp>) ...file next to the original. The conflicted copy holds the correct content; the original becomes the container.If a conflict is a precondition, then the interesting question is why the conflict resolution path fetches the raw container rather than the decrypted stream.
Related, but not duplicates
I searched before filing:
nextcloud/server#54594 "Encryption logic broken" is a different defect -fclose()on a string inStorage/Wrapper/Encryption.php, hit when resuming an interrupteddecrypt-all.One thing to get out of the way early: "just turn off server-side encryption" is not a cheap workaround here.
decrypt-allhas several open reports of leaving files encrypted or only partially decrypted (#16597, #18709, #54594).A note for anyone hitting this
The good content is in the conflicted copy, and for a versioned file also in the repository. A cheap detector is to check the first 24 bytes of each file for
HBEGIN:oc_encryption; a full tree of 14000 files takes under a second that way.Environment
nextcloud-client 2:34.0.3-1), Manjaro Linux, kernel 6.18.49useLegacyFileKey:false)