Skip to content

Add optional chunk encryption to stores#371

Open
folbricht wants to merge 4 commits into
masterfrom
chunk-encryption
Open

Add optional chunk encryption to stores#371
folbricht wants to merge 4 commits into
masterfrom
chunk-encryption

Conversation

@folbricht

@folbricht folbricht commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Revives #182, rebased onto the current codebase and reworked based on the discussion in that PR.

Chunks can be encrypted at rest on a per-store basis using XChaCha20-Poly1305 (default) or AES-256-GCM. Encryption is implemented as a converter layer on top of the framework from #181: it is applied after compression when writing to a store and reversed when reading. Encrypted chunks are stored with a file extension containing the algorithm and a key ID (the leading 4 bytes of SHA256 of the key), e.g. <hash>.cacnk.xchacha20-poly1305-e1e2b9f0, so chunks with different settings or keys can coexist in the same store. prune and verify only consider chunks matching the store's configured extension.

The main change compared to #182: instead of deriving the key from a password with SHA256, the user provides the raw 256-bit key hex-encoded (e.g. generated with openssl rand -hex 32). As discussed in the original PR, password-derived keys are only as strong as the password and can be brute-forced cheaply, especially since the key ID in the chunk file name acts as a fast verification oracle. Requiring the full-entropy key eliminates that class of problem, and makes the key ID in the extension harmless.

Configuration:

  • encryption, encryption-key, and encryption-algorithm in store-options
  • DESYNC_ENCRYPTION_KEY environment variable as fallback for the key, to keep it out of config files
  • chunk-server --encryption / --encryption-key to serve/accept encrypted chunks, independent of how the upstream store is configured. The env variable supplies the key but never enables encryption on its own, and enabling encryption without a key is an error

Also included from the rebase: the converter interface now determines chunk file extensions (storageExtension()), replacing the CompressedChunkExt/UncompressedChunkExt constants, and the now-unused hasCompression() helper was removed.

Note: encryption protects chunk contents only. Chunk file names remain content hashes of the plain data, so an observer who already knows a plaintext can confirm its presence in a store. Integrity comes from the regular chunk content validation rather than from the AEAD, which authenticates ciphertext under the key but does not bind it to the chunk name. Both properties are documented in the README, including the guidance to only use skip-verify where a downstream reader still validates.

folbricht added 4 commits July 9, 2026 09:56
Chunks can now be encrypted at rest on a per-store basis using
XChaCha20-Poly1305 (default) or AES-256-GCM. Encryption is implemented
as a converter layer, applied after compression when writing to a store
and reversed when reading. Encrypted chunks carry a file extension with
the algorithm name and a key ID (leading 4 bytes of SHA256(key)) so that
chunks with different settings or keys can share a store.

The 256-bit encryption key is provided hex-encoded, either in the
store-options of the config file (encryption-key), via the
DESYNC_ENCRYPTION_KEY environment variable, or with the chunk-server
--encryption-key option. Requiring the raw key rather than deriving one
from a password avoids weak, brute-forceable passwords entirely; a
suitable key can be generated with `openssl rand -hex 32`.

This revives and reworks the changes from #182 on top of the current
codebase.
- Move the Compressor converter from compress.go into converter.go so
  the datadog build variant compiles again; compress.go is only built
  without the datadog tag.
- Reject store options that set encryption-key or encryption-algorithm
  without enabling encryption instead of silently writing plaintext.
- Require an explicit flag to enable encryption in chunk-server. The
  DESYNC_ENCRYPTION_KEY environment variable alone no longer switches
  the wire format, it is only consulted once encryption is enabled with
  --encryption, --encryption-key or --encryption-algorithm. Enabling
  encryption without a key is now an error rather than silently serving
  plaintext.
- Fail when encryption is configured on casync protocol (ssh://) stores
  which don't support it, rather than silently ignoring it.
- Reject chunk requests whose name carries an unexpected extension with
  a clear error again. The suffix check alone was a no-op for servers
  without compression and encryption, and such requests only failed
  deep in the chunk ID parser.
Encryption provides confidentiality while integrity relies on the
regular chunk content validation, since the AEAD ciphertext is
authenticated under the key but not bound to the chunk name. Spell
that out in the encryption docs, including the consequence that
skip-verify should only be used where a downstream reader still
validates chunks.
- StorageConverters returns the named Converters type instead of
  []converter, giving callers access to its methods without a cast.
- Derive the chunk file extension from the converters once at store
  construction rather than rebuilding the string on every chunk
  operation. All stores and the HTTP handler now follow the pattern
  the SFTP store already used, and SFTPStore.Prune uses the pooled
  base's extension instead of deriving it a second way.
- Collapse the two identical AEAD converter implementations into one
  aeadConverter parameterized by algorithm, keyed comparison now
  includes the algorithm name. Adding an algorithm only requires a
  constructor and a case in StorageConverters.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant