100 lines
2.7 KiB
Markdown
100 lines
2.7 KiB
Markdown
|
||
### Cargo.toml
|
||
|
||
```toml
|
||
[dependencies]
|
||
chacha20poly1305 = { version = "0.10", features = ["xchacha20"] }
|
||
rand = "0.8"
|
||
sha2 = "0.10"
|
||
```
|
||
|
||
### `crypto_factory.rs`
|
||
|
||
```rust
|
||
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
|
||
|
||
```rust
|
||
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);
|
||
}
|
||
```
|
||
|
||
That’s it: `new(secret)`, `encrypt(bytes)`, `decrypt(bytes)`.
|
||
You can stash the returned `blob` directly in your storage layer behind Redis.
|