1use chacha20poly1305::{
12 aead::{Aead, KeyInit},
13 ChaCha20Poly1305, Nonce,
14};
15use sha2::{Digest, Sha256};
16use snow::{params::NoiseParams, Builder, HandshakeState, TransportState};
17use std::sync::OnceLock;
18use x25519_dalek::{PublicKey, StaticSecret};
19
20pub const NOISE_PATTERN: &str = "Noise_NK_25519_ChaChaPoly_SHA256";
22
23fn noise_params() -> &'static NoiseParams {
24 static PARAMS: OnceLock<NoiseParams> = OnceLock::new();
25 PARAMS.get_or_init(|| NOISE_PATTERN.parse().expect("Noise_NK params"))
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub struct PublicKeyBytes(pub [u8; 32]);
31
32impl PublicKeyBytes {
33 pub fn as_bytes(&self) -> &[u8; 32] {
34 &self.0
35 }
36}
37
38#[derive(Clone)]
40pub struct RestreamerKeys {
41 c2_secret: StaticSecret,
42 c2_public: PublicKey,
43 hq_recipient_public: PublicKey,
44 hq_recipient_public_prev: Option<PublicKey>,
45}
46
47impl RestreamerKeys {
48 pub fn generate(hq_recipient_public: PublicKeyBytes) -> Self {
49 let mut seed = [0u8; 32];
50 getrandom::getrandom(&mut seed).expect("os rng");
51 Self::from_seeds(seed, hq_recipient_public.0)
52 }
53
54 pub fn from_seeds(c2_seed: [u8; 32], hq_public: [u8; 32]) -> Self {
55 let c2_secret = StaticSecret::from(c2_seed);
56 let c2_public = PublicKey::from(&c2_secret);
57 Self {
58 c2_secret,
59 c2_public,
60 hq_recipient_public: PublicKey::from(hq_public),
61 hq_recipient_public_prev: None,
62 }
63 }
64
65 pub fn upstream_public_keys(&self) -> (PublicKeyBytes, PublicKeyBytes) {
66 (
67 PublicKeyBytes(*self.c2_public.as_bytes()),
68 PublicKeyBytes(*self.hq_recipient_public.as_bytes()),
69 )
70 }
71
72 pub fn rotate_hq_recipient(&mut self, new_public: PublicKeyBytes) {
73 self.hq_recipient_public_prev = Some(self.hq_recipient_public);
74 self.hq_recipient_public = PublicKey::from(new_public.0);
75 }
76
77 pub fn end_rotation_window(&mut self) {
78 self.hq_recipient_public_prev = None;
79 }
80
81 pub fn hq_recipient_public(&self) -> PublicKeyBytes {
82 PublicKeyBytes(*self.hq_recipient_public.as_bytes())
83 }
84
85 pub fn previous_hq_recipient_public(&self) -> Option<PublicKeyBytes> {
86 self.hq_recipient_public_prev
87 .map(|k| PublicKeyBytes(*k.as_bytes()))
88 }
89
90 pub fn c2_public(&self) -> PublicKeyBytes {
91 PublicKeyBytes(*self.c2_public.as_bytes())
92 }
93}
94
95#[derive(Clone)]
97pub struct HqRecipientSecret {
98 private: [u8; 32],
99 public: [u8; 32],
100}
101
102impl HqRecipientSecret {
103 pub fn generate() -> Self {
104 let mut seed = [0u8; 32];
105 getrandom::getrandom(&mut seed).expect("os rng");
106 Self::from_seed(seed)
107 }
108
109 pub fn from_seed(seed: [u8; 32]) -> Self {
110 let secret = StaticSecret::from(seed);
111 let public = PublicKey::from(&secret);
112 Self {
113 private: seed,
114 public: *public.as_bytes(),
115 }
116 }
117
118 pub fn public_bytes(&self) -> PublicKeyBytes {
119 PublicKeyBytes(self.public)
120 }
121
122 pub fn seed_bytes(&self) -> [u8; 32] {
124 self.private
125 }
126}
127
128pub struct NoiseInitiator {
130 state: HandshakeState,
131}
132
133impl NoiseInitiator {
134 pub fn new(hq_recipient_public: PublicKeyBytes) -> Result<Self, CryptoError> {
135 let state = Builder::new(noise_params().clone())
136 .remote_public_key(&hq_recipient_public.0)
137 .map_err(noise_err)?
138 .build_initiator()
139 .map_err(noise_err)?;
140 Ok(Self { state })
141 }
142
143 pub fn write_message1(&mut self) -> Result<Vec<u8>, CryptoError> {
145 let mut buf = vec![0u8; 64];
146 let n = self.state.write_message(&[], &mut buf).map_err(noise_err)?;
147 buf.truncate(n);
148 Ok(buf)
149 }
150
151 pub fn read_message2(self, msg: &[u8]) -> Result<NoiseTransport, CryptoError> {
153 let mut state = self.state;
154 let mut payload = [0u8; 64];
155 let _ = state.read_message(msg, &mut payload).map_err(noise_err)?;
156 let transport = state.into_transport_mode().map_err(noise_err)?;
157 Ok(NoiseTransport { state: transport })
158 }
159}
160
161pub struct NoiseResponder {
163 state: HandshakeState,
164}
165
166impl NoiseResponder {
167 pub fn new(hq: &HqRecipientSecret) -> Result<Self, CryptoError> {
168 let state = Builder::new(noise_params().clone())
169 .local_private_key(&hq.private)
170 .map_err(noise_err)?
171 .build_responder()
172 .map_err(noise_err)?;
173 Ok(Self { state })
174 }
175
176 pub fn complete(self, message1: &[u8]) -> Result<(Vec<u8>, NoiseTransport), CryptoError> {
178 let mut state = self.state;
179 let mut payload = [0u8; 64];
180 let _ = state.read_message(message1, &mut payload).map_err(noise_err)?;
181 let mut msg2 = vec![0u8; 64];
182 let n = state.write_message(&[], &mut msg2).map_err(noise_err)?;
183 msg2.truncate(n);
184 let transport = state.into_transport_mode().map_err(noise_err)?;
185 Ok((msg2, NoiseTransport { state: transport }))
186 }
187}
188
189pub struct NoiseTransport {
191 state: TransportState,
192}
193
194impl NoiseTransport {
195 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
200 if plaintext.len() <= NOISE_MAX_PLAINTEXT {
201 return self.encrypt_one(plaintext);
202 }
203 let mut out = Vec::with_capacity(plaintext.len() + plaintext.len() / NOISE_MAX_PLAINTEXT * 20 + 20);
204 for chunk in plaintext.chunks(NOISE_MAX_PLAINTEXT) {
205 let ct = self.encrypt_one(chunk)?;
206 out.extend_from_slice(&(ct.len() as u32).to_be_bytes());
207 out.extend_from_slice(&ct);
208 }
209 Ok(out)
210 }
211
212 pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
213 if ciphertext.len() <= NOISE_MAX_MESSAGE {
214 return self.decrypt_one(ciphertext);
215 }
216 let mut out = Vec::with_capacity(ciphertext.len());
217 let mut rest = ciphertext;
218 while !rest.is_empty() {
219 if rest.len() < 4 {
220 return Err(noise_err(snow::Error::Input));
221 }
222 let n = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
223 if n == 0 || n > NOISE_MAX_MESSAGE || rest.len() < 4 + n {
224 return Err(noise_err(snow::Error::Input));
225 }
226 out.extend_from_slice(&self.decrypt_one(&rest[4..4 + n])?);
227 rest = &rest[4 + n..];
228 }
229 Ok(out)
230 }
231
232 fn encrypt_one(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
233 let mut buf = vec![0u8; plaintext.len() + 16];
234 let n = self.state.write_message(plaintext, &mut buf).map_err(noise_err)?;
235 buf.truncate(n);
236 Ok(buf)
237 }
238
239 fn decrypt_one(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
240 let mut buf = vec![0u8; ciphertext.len()];
241 let n = self.state.read_message(ciphertext, &mut buf).map_err(noise_err)?;
242 buf.truncate(n);
243 Ok(buf)
244 }
245}
246
247pub const NOISE_MAX_MESSAGE: usize = 65_535;
249pub const NOISE_MAX_PLAINTEXT: usize = NOISE_MAX_MESSAGE - 16;
251
252pub struct NestedSeal {
254 pub outer: Vec<u8>,
256 pub inner_ciphertext: Vec<u8>,
258 pub hq_transport: NoiseTransport,
260 pub node_transport: NoiseTransport,
262 pub handshake_msg1: Vec<u8>,
263 pub handshake_msg2: Vec<u8>,
264}
265
266pub fn noise_session_seal(
268 keys: &RestreamerKeys,
269 hq: &HqRecipientSecret,
270 plaintext: &[u8],
271) -> Result<NestedSeal, CryptoError> {
272 let mut initiator = NoiseInitiator::new(keys.hq_recipient_public())?;
273 let msg1 = initiator.write_message1()?;
274 let responder = NoiseResponder::new(hq)?;
275 let (msg2, hq_transport) = responder.complete(&msg1)?;
276 let mut node_transport = initiator.read_message2(&msg2)?;
277 let inner_ciphertext = node_transport.encrypt(plaintext)?;
278 let outer = outer_seal(&keys.c2_public, &inner_ciphertext)?;
279 Ok(NestedSeal {
280 outer,
281 inner_ciphertext,
282 hq_transport,
283 node_transport,
284 handshake_msg1: msg1,
285 handshake_msg2: msg2,
286 })
287}
288
289pub fn core_open_inner(
291 transport: &mut NoiseTransport,
292 ciphertext: &[u8],
293) -> Result<Vec<u8>, CryptoError> {
294 transport.decrypt(ciphertext)
295}
296
297pub fn node_seal_outer(c2_public: PublicKeyBytes, inner: &[u8]) -> Result<Vec<u8>, CryptoError> {
299 outer_seal(&PublicKey::from(c2_public.0), inner)
300}
301
302pub fn c2_strip_outer(keys: &RestreamerKeys, sealed: &[u8]) -> Result<Vec<u8>, CryptoError> {
304 outer_open(&keys.c2_secret, sealed)
305}
306
307pub fn c2_cannot_complete_as_hq(keys: &RestreamerKeys, handshake_msg1: &[u8]) -> bool {
309 let fake = HqRecipientSecret {
310 private: keys.c2_secret.to_bytes(),
311 public: *PublicKey::from(&keys.c2_secret).as_bytes(),
312 };
313 NoiseResponder::new(&fake)
314 .and_then(|r| r.complete(handshake_msg1))
315 .is_err()
316}
317
318pub fn c2_cannot_open_inner(keys: &RestreamerKeys, inner_ciphertext: &[u8]) -> bool {
321 let _ = keys;
325 outer_open(&keys.c2_secret, inner_ciphertext).is_err()
327}
328
329fn outer_seal(recipient: &PublicKey, plaintext: &[u8]) -> Result<Vec<u8>, CryptoError> {
330 let mut eph_seed = [0u8; 32];
331 getrandom::getrandom(&mut eph_seed).map_err(|e| CryptoError::Rng(e.to_string()))?;
332 let eph = StaticSecret::from(eph_seed);
333 let eph_pub = PublicKey::from(&eph);
334 let shared = eph.diffie_hellman(recipient);
335 let key = kdf(shared.as_bytes());
336 let cipher = ChaCha20Poly1305::new_from_slice(&key).map_err(|_| CryptoError::BadKey)?;
337 let mut nonce_bytes = [0u8; 12];
338 getrandom::getrandom(&mut nonce_bytes).map_err(|e| CryptoError::Rng(e.to_string()))?;
339 let nonce = Nonce::from_slice(&nonce_bytes);
340 let ct = cipher
341 .encrypt(nonce, plaintext)
342 .map_err(|_| CryptoError::Encrypt)?;
343 let mut out = Vec::with_capacity(32 + 12 + ct.len());
344 out.extend_from_slice(eph_pub.as_bytes());
345 out.extend_from_slice(&nonce_bytes);
346 out.extend_from_slice(&ct);
347 Ok(out)
348}
349
350fn outer_open(secret: &StaticSecret, sealed: &[u8]) -> Result<Vec<u8>, CryptoError> {
351 if sealed.len() < 32 + 12 + 16 {
352 return Err(CryptoError::Truncated);
353 }
354 let mut eph_bytes = [0u8; 32];
355 eph_bytes.copy_from_slice(&sealed[..32]);
356 let eph_pub = PublicKey::from(eph_bytes);
357 let nonce = Nonce::from_slice(&sealed[32..44]);
358 let ct = &sealed[44..];
359 let shared = secret.diffie_hellman(&eph_pub);
360 let key = kdf(shared.as_bytes());
361 let cipher = ChaCha20Poly1305::new_from_slice(&key).map_err(|_| CryptoError::BadKey)?;
362 cipher
363 .decrypt(nonce, ct)
364 .map_err(|_| CryptoError::Decrypt)
365}
366
367fn kdf(shared: &[u8]) -> [u8; 32] {
368 let mut hasher = Sha256::new();
369 hasher.update(b"hatchery-restreamer-outer-v1");
370 hasher.update(shared);
371 let digest = hasher.finalize();
372 let mut out = [0u8; 32];
373 out.copy_from_slice(&digest);
374 out
375}
376
377fn noise_err(err: snow::Error) -> CryptoError {
378 CryptoError::Noise(err.to_string())
379}
380
381#[derive(Clone, Debug, PartialEq, Eq)]
382pub enum CryptoError {
383 Rng(String),
384 BadKey,
385 Encrypt,
386 Decrypt,
387 Truncated,
388 Noise(String),
389}
390
391impl std::fmt::Display for CryptoError {
392 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393 match self {
394 Self::Rng(msg) => write!(f, "rng: {msg}"),
395 Self::BadKey => write!(f, "bad key"),
396 Self::Encrypt => write!(f, "encrypt failed"),
397 Self::Decrypt => write!(f, "decrypt failed"),
398 Self::Truncated => write!(f, "truncated sealed box"),
399 Self::Noise(msg) => write!(f, "noise: {msg}"),
400 }
401 }
402}
403
404impl std::error::Error for CryptoError {}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409
410 #[test]
411 fn noise_nk_handshake_and_transport() {
412 let hq = HqRecipientSecret::from_seed([7u8; 32]);
413 let mut initiator = NoiseInitiator::new(hq.public_bytes()).unwrap();
414 let msg1 = initiator.write_message1().unwrap();
415 let responder = NoiseResponder::new(&hq).unwrap();
416 let (msg2, mut hq_tx) = responder.complete(&msg1).unwrap();
417 let mut node_tx = initiator.read_message2(&msg2).unwrap();
418 let ct = node_tx.encrypt(b"agent-message-body").unwrap();
419 assert!(!ct.windows(b"agent-message-body".len()).any(|w| w == b"agent-message-body"));
420 assert_eq!(hq_tx.decrypt(&ct).unwrap(), b"agent-message-body");
421 }
422
423 #[test]
424 fn nested_roundtrip_and_c2_blind_to_inner() {
425 let hq = HqRecipientSecret::from_seed([7u8; 32]);
426 let keys = RestreamerKeys::from_seeds([3u8; 32], hq.public_bytes().0);
427 let (c2_pub, hq_pub) = keys.upstream_public_keys();
428 assert_eq!(c2_pub, keys.c2_public());
429 assert_eq!(hq_pub, hq.public_bytes());
430
431 let plain = b"agent-message-body";
432 let sealed = noise_session_seal(&keys, &hq, plain).unwrap();
433 let inner = c2_strip_outer(&keys, &sealed.outer).unwrap();
434 assert_eq!(inner, sealed.inner_ciphertext);
435 assert!(c2_cannot_open_inner(&keys, &inner));
436 assert!(c2_cannot_complete_as_hq(&keys, &sealed.handshake_msg1));
437 assert!(!inner.windows(plain.len()).any(|w| w == plain));
438 let mut hq_tx = sealed.hq_transport;
439 assert_eq!(core_open_inner(&mut hq_tx, &inner).unwrap(), plain);
440 }
441
442 fn transport_pair() -> (NoiseTransport, NoiseTransport) {
443 let hq = HqRecipientSecret::from_seed([11u8; 32]);
444 let mut init = NoiseInitiator::new(hq.public_bytes()).unwrap();
445 let msg1 = init.write_message1().unwrap();
446 let (msg2, resp) = NoiseResponder::new(&hq).unwrap().complete(&msg1).unwrap();
447 (init.read_message2(&msg2).unwrap(), resp)
448 }
449
450 #[test]
451 fn large_plaintexts_are_chunked_and_small_stay_single_message() {
452 let (mut a, mut b) = transport_pair();
453 for len in [0usize, 1, NOISE_MAX_PLAINTEXT, NOISE_MAX_PLAINTEXT + 1, 300_000] {
454 let plain: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
455 let ct = a.encrypt(&plain).unwrap();
456 if len <= NOISE_MAX_PLAINTEXT {
457 assert_eq!(ct.len(), len + 16, "single message is wire-compatible");
458 } else {
459 assert!(ct.len() > NOISE_MAX_MESSAGE);
460 }
461 assert_eq!(b.decrypt(&ct).unwrap(), plain, "len {len}");
462 let back = b.encrypt(&plain).unwrap();
464 assert_eq!(a.decrypt(&back).unwrap(), plain);
465 }
466 let mut bad = a.encrypt(&vec![7u8; 100_000]).unwrap();
467 bad.truncate(bad.len() - 3);
468 assert!(b.decrypt(&bad).is_err());
469 }
470
471 #[test]
472 fn tampered_inner_rejected_by_core() {
473 let hq = HqRecipientSecret::from_seed([9u8; 32]);
474 let keys = RestreamerKeys::from_seeds([1u8; 32], hq.public_bytes().0);
475 let sealed = noise_session_seal(&keys, &hq, b"secret").unwrap();
476 let mut inner = c2_strip_outer(&keys, &sealed.outer).unwrap();
477 let last = inner.len() - 1;
478 inner[last] ^= 0x5a;
479 let mut hq_tx = sealed.hq_transport;
480 assert!(core_open_inner(&mut hq_tx, &inner).is_err());
481 }
482
483 #[test]
484 fn rotation_window_accepts_new_key() {
485 let hq_old = HqRecipientSecret::from_seed([2u8; 32]);
486 let hq_new = HqRecipientSecret::from_seed([4u8; 32]);
487 let mut keys = RestreamerKeys::from_seeds([5u8; 32], hq_old.public_bytes().0);
488 keys.rotate_hq_recipient(hq_new.public_bytes());
489 let sealed = noise_session_seal(&keys, &hq_new, b"after-rotate").unwrap();
490 let inner = c2_strip_outer(&keys, &sealed.outer).unwrap();
491 let mut hq_tx = sealed.hq_transport;
492 assert_eq!(core_open_inner(&mut hq_tx, &inner).unwrap(), b"after-rotate");
493 assert!(NoiseResponder::new(&hq_old)
495 .unwrap()
496 .complete(&sealed.handshake_msg1)
497 .is_err());
498 keys.end_rotation_window();
499 assert!(keys.previous_hq_recipient_public().is_none());
500 }
501
502 #[test]
503 fn wrong_recipient_cannot_complete_handshake() {
504 let hq = HqRecipientSecret::from_seed([1u8; 32]);
505 let other = HqRecipientSecret::from_seed([2u8; 32]);
506 let mut initiator = NoiseInitiator::new(hq.public_bytes()).unwrap();
507 let msg1 = initiator.write_message1().unwrap();
508 let responder = NoiseResponder::new(&other).unwrap();
509 assert!(responder.complete(&msg1).is_err());
510 }
511}