ps-cypher 0.1.0-30

Convergent encryption with ChaCha20-Poly1305, zstd, and Reed-Solomon ECC
Documentation

ps-cypher

Convergent encryption with ChaCha20-Poly1305, zstd, and Reed-Solomon ECC.

Scheme

  • The encryption key is the hash of the plaintext (ps-hash): the ChaCha20 key is the hash's 32-byte digest, and the nonce is derived from the hash's parity bytes.
  • The plaintext is compressed deterministically (zstd level 7 via ps-compress) before encryption.
  • The ciphertext is Reed-Solomon encoded (ps-ecc), so up to 12 corrupted bytes per codeword are repaired transparently during decryption.
  • A four-byte XXH64 tag of the ECC codeword is appended, so validate can recognise a pristine ps-cypher codeword without the key and tell it apart from other ps-ecc codewords.
  • Identical inputs therefore always produce identical ciphertexts, keys, and hashes, which enables content-addressed deduplication of encrypted data.
  • decrypt re-hashes the decrypted data and fails with KeyMismatch unless it hashes to the decryption key, so a successful decryption returns exactly the data that produced the key.

Example

use ps_cypher::{decrypt, encrypt};

let encrypted = encrypt(b"example payload")?;
let decrypted = decrypt(&encrypted.bytes, &encrypted.key)?;

assert_eq!(&*decrypted, b"example payload");
# Ok::<(), Box<dyn std::error::Error>>(())

Security caveats

Convergent encryption is deterministic by design, which leaks information that a randomized cipher would not:

  • Plaintext equality: identical plaintexts encrypt to identical ciphertexts, so an observer learns when two stored objects are equal.
  • Confirmation and dictionary attacks: anyone who can guess a plaintext can encrypt the guess and compare ciphertexts, confirming whether it is stored. Low-entropy data is therefore not confidential against an adversary who can enumerate candidates.
  • Length leakage: the ciphertext length reveals the compressed size of the plaintext, i.e., its length and compressibility.
  • validate is not authentication: the tag is an unkeyed checksum, so Validity::Pristine means "unmodified since encrypt", not "produced by a trusted party". decrypt ignores the tag entirely. Tampering is detected by the Poly1305 tag during decrypt, and the decrypted data is additionally verified to hash to the decryption key.