1use std::fmt;
4use std::sync::{Arc, Mutex, MutexGuard};
5
6use softrelay_noise::{
7 MAX_NOISE_MESSAGE_SIZE as UPSTREAM_MAX_NOISE_MESSAGE_SIZE, NOISE_MAX_CIPHERTEXT,
8 NOISE_MAX_PLAINTEXT, NoiseNkInitiator, NoiseSession,
9};
10
11use crate::SoftchatError;
12
13pub const NOISE_PROTOCOL_NAME: &str = "Noise_NK_25519_ChaChaPoly_SHA256";
15pub const MAX_NOISE_MESSAGE_SIZE: usize = UPSTREAM_MAX_NOISE_MESSAGE_SIZE;
17pub const MAX_NOISE_FRAME_PLAINTEXT: usize = NOISE_MAX_PLAINTEXT;
19pub const MAX_NOISE_FRAME_CIPHERTEXT: usize = NOISE_MAX_CIPHERTEXT;
21
22#[cfg_attr(feature = "native-bindings", derive(uniffi::Object))]
28pub struct NoiseClientHandshake {
29 message_one: Vec<u8>,
30 initiator: Mutex<Option<NoiseNkInitiator>>,
31}
32
33impl fmt::Debug for NoiseClientHandshake {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 formatter
36 .debug_struct("NoiseClientHandshake")
37 .field("message_one_bytes", &self.message_one.len())
38 .finish_non_exhaustive()
39 }
40}
41
42#[cfg_attr(feature = "native-bindings", uniffi::export)]
43impl NoiseClientHandshake {
44 #[cfg_attr(feature = "native-bindings", uniffi::constructor)]
51 pub fn new(
52 relay_static_public_key: Vec<u8>,
53 request_chunking: bool,
54 ) -> Result<Self, SoftchatError> {
55 let relay_static_public_key: [u8; 32] = relay_static_public_key
56 .try_into()
57 .map_err(|_| SoftchatError::InvalidNoiseHandshake)?;
58 let (message_one, initiator) =
59 NoiseNkInitiator::initiate(relay_static_public_key, request_chunking)
60 .map_err(|_| SoftchatError::InvalidNoiseHandshake)?;
61 Ok(Self {
62 message_one,
63 initiator: Mutex::new(Some(initiator)),
64 })
65 }
66
67 #[must_use]
69 pub fn message_one(&self) -> Vec<u8> {
70 self.message_one.clone()
71 }
72
73 pub fn complete(&self, message_two: Vec<u8>) -> Result<Arc<NoiseTransport>, SoftchatError> {
80 let initiator = lock(&self.initiator)
81 .take()
82 .ok_or(SoftchatError::NoiseHandshakeConsumed)?;
83 let result = initiator
84 .process_message2(&message_two)
85 .map_err(|_| SoftchatError::InvalidNoiseHandshake)?;
86 let chunking_enabled = result.chunking_enabled();
87 Ok(Arc::new(NoiseTransport {
88 session: Mutex::new(Some(NoiseSession::new(result))),
89 chunking_enabled,
90 }))
91 }
92}
93
94#[cfg_attr(feature = "native-bindings", derive(uniffi::Object))]
99pub struct NoiseTransport {
100 session: Mutex<Option<NoiseSession>>,
101 chunking_enabled: bool,
102}
103
104impl fmt::Debug for NoiseTransport {
105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106 formatter
107 .debug_struct("NoiseTransport")
108 .field("chunking_enabled", &self.chunking_enabled)
109 .finish_non_exhaustive()
110 }
111}
112
113#[cfg_attr(feature = "native-bindings", uniffi::export)]
114impl NoiseTransport {
115 #[must_use]
117 pub const fn chunking_enabled(&self) -> bool {
118 self.chunking_enabled
119 }
120
121 pub fn encrypt(&self, plaintext: Vec<u8>) -> Result<Vec<Vec<u8>>, SoftchatError> {
128 let mut guard = lock(&self.session);
129 guard
130 .as_mut()
131 .ok_or(SoftchatError::NoiseSessionClosed)?
132 .encrypt(&plaintext)
133 .map_err(|_| SoftchatError::NoiseTransportFailed)
134 }
135
136 pub fn decrypt(&self, ciphertext: Vec<u8>) -> Result<Option<Vec<u8>>, SoftchatError> {
146 let mut guard = lock(&self.session);
147 guard
148 .as_mut()
149 .ok_or(SoftchatError::NoiseSessionClosed)?
150 .decrypt(&ciphertext)
151 .map_err(|_| SoftchatError::NoiseTransportFailed)
152 }
153
154 pub fn shutdown(&self) {
156 lock(&self.session).take();
157 }
158}
159
160fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
161 match mutex.lock() {
162 Ok(guard) => guard,
163 Err(poisoned) => poisoned.into_inner(),
164 }
165}
166
167#[cfg(test)]
168#[allow(clippy::unwrap_used)]
169mod tests {
170 use softrelay_noise::{NoiseNkResponder, NoiseSession, StaticSecret};
171
172 use super::*;
173
174 #[test]
175 fn exact_nk_profile_round_trips_and_closes_without_exporting_keys() {
176 let responder = NoiseNkResponder::new(StaticSecret::from([0x31; 32]));
177 let public_key = responder.static_public_key().to_vec();
178 let client = NoiseClientHandshake::new(public_key, true).unwrap();
179 let (message_two, server_result) = responder.respond(&client.message_one()).unwrap();
180 let client_transport = client.complete(message_two).unwrap();
181 let mut server_transport = NoiseSession::new(server_result);
182
183 let plaintext = vec![0x42; MAX_NOISE_FRAME_PLAINTEXT + 1_000];
184 let frames = client_transport.encrypt(plaintext.clone()).unwrap();
185 assert!(frames.len() > 1);
186 let mut received = None;
187 for frame in frames {
188 received = server_transport.decrypt(&frame).unwrap();
189 }
190 assert_eq!(received, Some(plaintext));
191 assert!(client_transport.chunking_enabled());
192
193 client_transport.shutdown();
194 assert!(matches!(
195 client_transport.encrypt(vec![1]),
196 Err(SoftchatError::NoiseSessionClosed)
197 ));
198 assert!(matches!(
199 client.complete(vec![0; 49]),
200 Err(SoftchatError::NoiseHandshakeConsumed)
201 ));
202 }
203
204 #[test]
205 fn rejects_bad_static_key_and_authenticated_transport_tampering() {
206 assert!(matches!(
207 NoiseClientHandshake::new(vec![0; 31], false),
208 Err(SoftchatError::InvalidNoiseHandshake)
209 ));
210
211 let responder = NoiseNkResponder::new(StaticSecret::from([0x32; 32]));
212 let client =
213 NoiseClientHandshake::new(responder.static_public_key().to_vec(), false).unwrap();
214 let (message_two, server_result) = responder.respond(&client.message_one()).unwrap();
215 let client_transport = client.complete(message_two).unwrap();
216 let mut server_transport = NoiseSession::new(server_result);
217 let mut frame = client_transport.encrypt(b"authenticated".to_vec()).unwrap()[0].clone();
218 frame[0] ^= 1;
219 assert!(server_transport.decrypt(&frame).is_err());
220 }
221}