herodb/instructions/encrypt.md
2025-08-16 10:53:48 +02:00

2.7 KiB
Raw Blame History

Cargo.toml

[dependencies]
chacha20poly1305 = { version = "0.10", features = ["xchacha20"] }
rand = "0.8"
sha2 = "0.10"

crypto_factory.rs

use chacha20poly1305::{
    aead::{Aead, KeyInit, OsRng},
    XChaCha20Poly1305, Key, XNonce,
};
use rand::RngCore;
use sha2::{Digest, Sha256};

const VERSION: u8 = 1;
const NONCE_LEN: usize = 24;
const TAG_LEN: usize = 16;

#[derive(Debug)]
pub enum CryptoError {
    Format,         // wrong length / header
    Version(u8),    // unknown version
    Decrypt,        // wrong key or corrupted data
}

/// Super-simple factory: new(secret) + encrypt(bytes) + decrypt(bytes)
pub struct CryptoFactory {
    key: Key<XChaCha20Poly1305>,
}

impl CryptoFactory {
    /// Accepts any secret bytes; turns them into a 32-byte key (SHA-256).
    /// (If your secret is already 32 bytes, this is still fine.)
    pub fn new<S: AsRef<[u8]>>(secret: S) -> Self {
        let mut h = Sha256::new();
        h.update(b"xchacha20poly1305-factory:v1"); // domain separation
        h.update(secret.as_ref());
        let digest = h.finalize(); // 32 bytes
        let key = Key::<XChaCha20Poly1305>::from_slice(&digest).to_owned();
        Self { key }
    }

    /// Output layout: [version:1][nonce:24][ciphertext||tag]
    pub fn encrypt(&self, plaintext: &[u8]) -> Vec<u8> {
        let cipher = XChaCha20Poly1305::new(&self.key);

        let mut nonce_bytes = [0u8; NONCE_LEN];
        OsRng.fill_bytes(&mut nonce_bytes);
        let nonce = XNonce::from_slice(&nonce_bytes);

        let mut out = Vec::with_capacity(1 + NONCE_LEN + plaintext.len() + TAG_LEN);
        out.push(VERSION);
        out.extend_from_slice(&nonce_bytes);

        let ct = cipher.encrypt(nonce, plaintext).expect("encrypt");
        out.extend_from_slice(&ct);
        out
    }

    pub fn decrypt(&self, blob: &[u8]) -> Result<Vec<u8>, CryptoError> {
        if blob.len() < 1 + NONCE_LEN + TAG_LEN {
            return Err(CryptoError::Format);
        }
        let ver = blob[0];
        if ver != VERSION {
            return Err(CryptoError::Version(ver));
        }

        let nonce = XNonce::from_slice(&blob[1..1 + NONCE_LEN]);
        let ct = &blob[1 + NONCE_LEN..];

        let cipher = XChaCha20Poly1305::new(&self.key);
        cipher.decrypt(nonce, ct).map_err(|_| CryptoError::Decrypt)
    }
}

Tiny usage example

fn main() {
    let f = CryptoFactory::new(b"super-secret-key-material");
    let val = b"\x00\xFFbinary\x01\x02\x03";

    let blob = f.encrypt(val);
    let roundtrip = f.decrypt(&blob).unwrap();

    assert_eq!(roundtrip, val);
}

Thats it: new(secret), encrypt(bytes), decrypt(bytes). You can stash the returned blob directly in your storage layer behind Redis.