brainwires_network/ipc/
crypto.rs1use anyhow::{Context, Result};
7use chacha20poly1305::{
8 ChaCha20Poly1305, Nonce,
9 aead::{Aead, KeyInit},
10};
11use rand::Rng;
12use sha2::{Digest, Sha256};
13use zeroize::Zeroizing;
14
15const KEY_SIZE: usize = 32;
17const NONCE_SIZE: usize = 12;
19
20pub struct IpcCipher {
23 cipher: ChaCha20Poly1305,
24}
25
26impl IpcCipher {
27 pub fn from_session_token(token: &str) -> Self {
32 let key = derive_key_from_token(token);
33 let cipher = ChaCha20Poly1305::new_from_slice(key.as_slice())
34 .expect("Key is always 32 bytes from SHA-256");
35 Self { cipher }
36 }
37
38 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
43 let mut nonce_bytes = [0u8; NONCE_SIZE];
45 rand::rng().fill_bytes(&mut nonce_bytes);
46 let nonce = Nonce::from_slice(&nonce_bytes);
47
48 let ciphertext = self
50 .cipher
51 .encrypt(nonce, plaintext)
52 .map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
53
54 let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len());
56 result.extend_from_slice(&nonce_bytes);
57 result.extend_from_slice(&ciphertext);
58
59 Ok(result)
60 }
61
62 pub fn decrypt(&self, encrypted: &[u8]) -> Result<Vec<u8>> {
66 if encrypted.len() < NONCE_SIZE {
67 anyhow::bail!("Encrypted message too short (missing nonce)");
68 }
69
70 let (nonce_bytes, ciphertext) = encrypted.split_at(NONCE_SIZE);
72 let nonce = Nonce::from_slice(nonce_bytes);
73
74 let plaintext = self
76 .cipher
77 .decrypt(nonce, ciphertext)
78 .map_err(|_| anyhow::anyhow!("Decryption failed (authentication failed)"))?;
79
80 Ok(plaintext)
81 }
82
83 pub fn encrypt_string(&self, plaintext: &str) -> Result<String> {
87 let encrypted = self.encrypt(plaintext.as_bytes())?;
88 Ok(base64::Engine::encode(
89 &base64::engine::general_purpose::STANDARD,
90 &encrypted,
91 ))
92 }
93
94 pub fn decrypt_string(&self, encrypted_b64: &str) -> Result<String> {
98 let encrypted =
99 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encrypted_b64)
100 .context("Invalid base64 encoding")?;
101
102 let plaintext = self.decrypt(&encrypted)?;
103 String::from_utf8(plaintext).context("Decrypted data is not valid UTF-8")
104 }
105}
106
107fn derive_key_from_token(token: &str) -> Zeroizing<[u8; KEY_SIZE]> {
109 let mut hasher = Sha256::new();
110 hasher.update(b"brainwires-ipc-v1:");
112 hasher.update(token.as_bytes());
113
114 let result = hasher.finalize();
115 let mut key = Zeroizing::new([0u8; KEY_SIZE]);
116 key.copy_from_slice(&result);
117 key
118}
119
120pub fn generate_random_key() -> Zeroizing<[u8; KEY_SIZE]> {
122 let mut key = Zeroizing::new([0u8; KEY_SIZE]);
123 rand::rng().fill_bytes(&mut *key);
124 key
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn test_encrypt_decrypt_roundtrip() {
133 let token = "test-session-token-12345";
134 let cipher = IpcCipher::from_session_token(token);
135
136 let plaintext = b"Hello, this is a secret message!";
137 let encrypted = cipher.encrypt(plaintext).unwrap();
138
139 assert_ne!(encrypted.as_slice(), plaintext);
141
142 let decrypted = cipher.decrypt(&encrypted).unwrap();
144 assert_eq!(decrypted, plaintext);
145 }
146
147 #[test]
148 fn test_encrypt_decrypt_string() {
149 let token = "string-test-token";
150 let cipher = IpcCipher::from_session_token(token);
151
152 let message = "This is a JSON message: {\"key\": \"value\"}";
153 let encrypted = cipher.encrypt_string(message).unwrap();
154
155 assert!(
157 encrypted
158 .chars()
159 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
160 );
161
162 let decrypted = cipher.decrypt_string(&encrypted).unwrap();
163 assert_eq!(decrypted, message);
164 }
165
166 #[test]
167 fn test_different_tokens_different_ciphertext() {
168 let cipher1 = IpcCipher::from_session_token("token1");
169 let cipher2 = IpcCipher::from_session_token("token2");
170
171 let plaintext = b"Same message";
172 let encrypted1 = cipher1.encrypt(plaintext).unwrap();
173 let encrypted2 = cipher2.encrypt(plaintext).unwrap();
174
175 assert_ne!(encrypted1, encrypted2);
178
179 assert!(cipher2.decrypt(&encrypted1).is_err());
181 assert!(cipher1.decrypt(&encrypted2).is_err());
182 }
183
184 #[test]
185 fn test_tamper_detection() {
186 let token = "tamper-test";
187 let cipher = IpcCipher::from_session_token(token);
188
189 let plaintext = b"Original message";
190 let mut encrypted = cipher.encrypt(plaintext).unwrap();
191
192 if let Some(byte) = encrypted.last_mut() {
194 *byte ^= 0xFF;
195 }
196
197 assert!(cipher.decrypt(&encrypted).is_err());
199 }
200
201 #[test]
202 fn test_empty_message() {
203 let token = "empty-test";
204 let cipher = IpcCipher::from_session_token(token);
205
206 let plaintext = b"";
207 let encrypted = cipher.encrypt(plaintext).unwrap();
208 let decrypted = cipher.decrypt(&encrypted).unwrap();
209
210 assert_eq!(decrypted, plaintext);
211 }
212
213 #[test]
214 fn test_large_message() {
215 let token = "large-test";
216 let cipher = IpcCipher::from_session_token(token);
217
218 let plaintext: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
220 let encrypted = cipher.encrypt(&plaintext).unwrap();
221 let decrypted = cipher.decrypt(&encrypted).unwrap();
222
223 assert_eq!(decrypted, plaintext);
224 }
225
226 #[test]
227 fn test_key_derivation_deterministic() {
228 let token = "deterministic-test";
229
230 let cipher1 = IpcCipher::from_session_token(token);
231 let cipher2 = IpcCipher::from_session_token(token);
232
233 let plaintext = b"Test message";
235 let encrypted = cipher1.encrypt(plaintext).unwrap();
236 let decrypted = cipher2.decrypt(&encrypted).unwrap();
237
238 assert_eq!(decrypted, plaintext);
239 }
240}