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.
 |