Skip to content

Client writes the raw server-side-encryption container into the local file, truncated #10773

Description

@follwersion

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:

HBEGIN:oc_encryption_module:OC_DEFAULT_MODULE:cipher:AES-256-CTR:signed:true:useLegacyFileKey:false:encoding:binary:HEND

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:

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.

Environment

  • Client 34.0.3 (nextcloud-client 2:34.0.3-1), Manjaro Linux, kernel 6.18.49
  • Server Nextcloud 33.0.8, managed hosting
  • Server-side encryption active, master-key mode (useLegacyFileKey:false)
  • No end-to-end encryption configured
  • No selective sync in the affected roots
  • The root where most specimens appeared is a two-way sync of roughly 55000 files

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions