1234567use snow::{8 Builder, Error, HandshakeState, TransportState,9 params::{CipherChoice, DHChoice, HashChoice, NoiseParams},10 resolvers::{CryptoResolver, DefaultResolver},11 types::{Cipher, Dh, Hash, Random},12};13use subtle::ConstantTimeEq;14use x25519_dalek::{PublicKey, StaticSecret};15use zeroize::{Zeroize, Zeroizing};1617const NK: &str = "Noise_NK_25519_ChaChaPoly_BLAKE2s";18const KK: &str = "Noise_KK_25519_ChaChaPoly_BLAKE2s";19pub const NOISE_PROLOGUE: &[u8] = b"peach-sync-v1";20const HASH_LEN: usize = 32;21const TAG_LEN: usize = 16;22const MAX_NOISE_MESSAGE: usize = 65_535;2324type CoreResult<T> = Result<T, String>;2526enum SessionState {27 Handshake(Option<HandshakeState>),28 Transport(TransportState),29 Closed,30}3132pub struct CoreNoiseSession {33 state: SessionState,34 handshake_hash: Option<[u8; HASH_LEN]>,35}3637impl CoreNoiseSession {38 pub fn nk_initiator(remote_public_key: &[u8]) -> CoreResult<Self> {39 Self::nk_initiator_inner(remote_public_key, None)40 }4142 #[doc(hidden)]43 pub fn nk_initiator_with_ephemeral_for_test(44 remote_public_key: &[u8],45 ephemeral_private_key: &[u8],46 ) -> CoreResult<Self> {47 Self::nk_initiator_inner(remote_public_key, Some(ephemeral_private_key))48 }4950 fn nk_initiator_inner(51 remote_public_key: &[u8],52 ephemeral_private_key: Option<&[u8]>,53 ) -> CoreResult<Self> {54 require_key(remote_public_key, "responder public key")?;55 let params: NoiseParams = NK.parse().map_err(display_error)?;56 let mut builder = core_builder(params)57 .remote_public_key(remote_public_key)58 .map_err(display_error)?;59 if let Some(ephemeral) = ephemeral_private_key {60 require_key(ephemeral, "initiator ephemeral private key")?;61 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);62 }63 let handshake = builder.build_initiator().map_err(display_error)?;64 Ok(Self::handshake(handshake))65 }6667 pub fn nk_responder(local_private_key: &[u8], local_public_key: &[u8]) -> CoreResult<Self> {68 Self::nk_responder_inner(local_private_key, local_public_key, None)69 }7071 #[doc(hidden)]72 pub fn nk_responder_with_ephemeral_for_test(73 local_private_key: &[u8],74 local_public_key: &[u8],75 ephemeral_private_key: &[u8],76 ) -> CoreResult<Self> {77 Self::nk_responder_inner(78 local_private_key,79 local_public_key,80 Some(ephemeral_private_key),81 )82 }8384 fn nk_responder_inner(85 local_private_key: &[u8],86 local_public_key: &[u8],87 ephemeral_private_key: Option<&[u8]>,88 ) -> CoreResult<Self> {89 require_matching_keypair(local_private_key, local_public_key)?;90 let private = Zeroizing::new(local_private_key.to_vec());91 let params: NoiseParams = NK.parse().map_err(display_error)?;92 let mut builder = core_builder(params)93 .local_private_key(private.as_slice())94 .map_err(display_error)?;95 if let Some(ephemeral) = ephemeral_private_key {96 require_key(ephemeral, "responder ephemeral private key")?;97 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);98 }99 let handshake = builder.build_responder().map_err(display_error)?;100 Ok(Self::handshake(handshake))101 }102103 pub fn kk_initiator(104 local_private_key: &[u8],105 local_public_key: &[u8],106 remote_public_key: &[u8],107 ) -> CoreResult<Self> {108 Self::kk_initiator_inner(local_private_key, local_public_key, remote_public_key, None)109 }110111 #[doc(hidden)]112 pub fn kk_initiator_with_ephemeral_for_test(113 local_private_key: &[u8],114 local_public_key: &[u8],115 remote_public_key: &[u8],116 ephemeral_private_key: &[u8],117 ) -> CoreResult<Self> {118 Self::kk_initiator_inner(119 local_private_key,120 local_public_key,121 remote_public_key,122 Some(ephemeral_private_key),123 )124 }125126 fn kk_initiator_inner(127 local_private_key: &[u8],128 local_public_key: &[u8],129 remote_public_key: &[u8],130 ephemeral_private_key: Option<&[u8]>,131 ) -> CoreResult<Self> {132 require_matching_keypair(local_private_key, local_public_key)?;133 require_key(remote_public_key, "responder public key")?;134 let private = Zeroizing::new(local_private_key.to_vec());135 let params: NoiseParams = KK.parse().map_err(display_error)?;136 let mut builder = core_builder(params)137 .local_private_key(private.as_slice())138 .map_err(display_error)?139 .remote_public_key(remote_public_key)140 .map_err(display_error)?;141 if let Some(ephemeral) = ephemeral_private_key {142 require_key(ephemeral, "initiator ephemeral private key")?;143 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);144 }145 let handshake = builder.build_initiator().map_err(display_error)?;146 Ok(Self::handshake(handshake))147 }148149 pub fn kk_responder(150 local_private_key: &[u8],151 local_public_key: &[u8],152 remote_public_key: &[u8],153 ) -> CoreResult<Self> {154 Self::kk_responder_inner(local_private_key, local_public_key, remote_public_key, None)155 }156157 #[doc(hidden)]158 pub fn kk_responder_with_ephemeral_for_test(159 local_private_key: &[u8],160 local_public_key: &[u8],161 remote_public_key: &[u8],162 ephemeral_private_key: &[u8],163 ) -> CoreResult<Self> {164 Self::kk_responder_inner(165 local_private_key,166 local_public_key,167 remote_public_key,168 Some(ephemeral_private_key),169 )170 }171172 fn kk_responder_inner(173 local_private_key: &[u8],174 local_public_key: &[u8],175 remote_public_key: &[u8],176 ephemeral_private_key: Option<&[u8]>,177 ) -> CoreResult<Self> {178 require_matching_keypair(local_private_key, local_public_key)?;179 require_key(remote_public_key, "initiator public key")?;180 let private = Zeroizing::new(local_private_key.to_vec());181 let params: NoiseParams = KK.parse().map_err(display_error)?;182 let mut builder = core_builder(params)183 .local_private_key(private.as_slice())184 .map_err(display_error)?185 .remote_public_key(remote_public_key)186 .map_err(display_error)?;187 if let Some(ephemeral) = ephemeral_private_key {188 require_key(ephemeral, "responder ephemeral private key")?;189 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);190 }191 let handshake = builder.build_responder().map_err(display_error)?;192 Ok(Self::handshake(handshake))193 }194195 fn handshake(handshake: HandshakeState) -> Self {196 Self {197 state: SessionState::Handshake(Some(handshake)),198 handshake_hash: None,199 }200 }201202 pub fn write_handshake(&mut self, payload: &[u8]) -> CoreResult<Vec<u8>> {203 let handshake = self.handshake_mut()?;204 if !handshake.is_my_turn() {205 return Err("Noise handshake write attempted out of turn".into());206 }207 let mut output = vec![0_u8; MAX_NOISE_MESSAGE];208 let size = handshake209 .write_message(payload, &mut output)210 .map_err(display_error)?;211 output.truncate(size);212 self.finish_if_ready()?;213 Ok(output)214 }215216 pub fn read_handshake(&mut self, message: &[u8]) -> CoreResult<Vec<u8>> {217 if message.len() > MAX_NOISE_MESSAGE {218 return Err("Noise handshake message exceeds 65535 bytes".into());219 }220 let handshake = self.handshake_mut()?;221 if handshake.is_my_turn() {222 return Err("Noise handshake read attempted out of turn".into());223 }224 let mut payload = vec![0_u8; MAX_NOISE_MESSAGE];225 let size = handshake226 .read_message(message, &mut payload)227 .map_err(display_error)?;228 payload.truncate(size);229 self.finish_if_ready()?;230 Ok(payload)231 }232233 pub fn encrypt(&mut self, plaintext: &[u8]) -> CoreResult<Vec<u8>> {234 if plaintext.len() > MAX_NOISE_MESSAGE - TAG_LEN {235 return Err("Noise plaintext exceeds transport limit".into());236 }237 let transport = self.transport_mut()?;238 let mut output = vec![0_u8; plaintext.len() + TAG_LEN];239 let size = transport240 .write_message(plaintext, &mut output)241 .map_err(display_error)?;242 output.truncate(size);243 Ok(output)244 }245246 pub fn decrypt(&mut self, ciphertext: &[u8]) -> CoreResult<Vec<u8>> {247 if ciphertext.len() > MAX_NOISE_MESSAGE {248 return Err("Noise ciphertext exceeds transport limit".into());249 }250 let transport = self.transport_mut()?;251 let mut output = vec![0_u8; ciphertext.len()];252 let size = transport253 .read_message(ciphertext, &mut output)254 .map_err(display_error)?;255 output.truncate(size);256 Ok(output)257 }258259 pub fn handshake_hash(&self) -> CoreResult<Vec<u8>> {260 self.handshake_hash261 .map(|hash| hash.to_vec())262 .ok_or_else(|| "Noise handshake is not complete".into())263 }264265 pub fn rekey_outgoing(&mut self) -> CoreResult<()> {266 self.transport_mut()?.rekey_outgoing();267 Ok(())268 }269270 pub fn rekey_incoming(&mut self) -> CoreResult<()> {271 self.transport_mut()?.rekey_incoming();272 Ok(())273 }274275 fn handshake_mut(&mut self) -> CoreResult<&mut HandshakeState> {276 match &mut self.state {277 SessionState::Handshake(Some(handshake)) => Ok(handshake),278 SessionState::Handshake(None) => Err("Noise handshake is transitioning".into()),279 SessionState::Transport(_) => Err("Noise handshake is already complete".into()),280 SessionState::Closed => Err("Noise session is closed".into()),281 }282 }283284 fn transport_mut(&mut self) -> CoreResult<&mut TransportState> {285 match &mut self.state {286 SessionState::Transport(transport) => Ok(transport),287 SessionState::Handshake(_) => Err("Noise handshake is not complete".into()),288 SessionState::Closed => Err("Noise session is closed".into()),289 }290 }291292 fn finish_if_ready(&mut self) -> CoreResult<()> {293 let ready = matches!(294 &self.state,295 SessionState::Handshake(Some(handshake)) if handshake.is_handshake_finished()296 );297 if !ready {298 return Ok(());299 }300301 let handshake = match std::mem::replace(&mut self.state, SessionState::Closed) {302 SessionState::Handshake(Some(handshake)) => handshake,303 _ => return Err("Noise session transition failed".into()),304 };305 let hash: [u8; HASH_LEN] = handshake306 .get_handshake_hash()307 .try_into()308 .map_err(|_| "Noise handshake hash must be 32 bytes".to_string())?;309 let transport = handshake.into_transport_mode().map_err(display_error)?;310 self.handshake_hash = Some(hash);311 self.state = SessionState::Transport(transport);312 Ok(())313 }314}315316impl Drop for CoreNoiseSession {317 fn drop(&mut self) {318 if let Some(hash) = &mut self.handshake_hash {319 hash.zeroize();320 }321 self.state = SessionState::Closed;322 }323}324325fn require_key(key: &[u8], label: &str) -> CoreResult<()> {326 if key.len() != 32 {327 return Err(format!("{label} must be 32 bytes"));328 }329 Ok(())330}331332fn require_matching_keypair(private_key: &[u8], public_key: &[u8]) -> CoreResult<()> {333 require_key(private_key, "local private key")?;334 require_key(public_key, "local public key")?;335 let private: [u8; 32] = private_key.try_into().expect("length checked");336 let derived = PublicKey::from(&StaticSecret::from(private));337 if derived.as_bytes().ct_eq(public_key).unwrap_u8() != 1 {338 return Err("local public key does not match local private key".into());339 }340 Ok(())341}342343fn display_error(error: impl std::fmt::Display) -> String {344 error.to_string()345}346347fn core_builder(params: NoiseParams) -> Builder<'static> {348 Builder::with_resolver(params, Box::new(RejectZeroResolver))349 .prologue(NOISE_PROLOGUE)350 .expect("fixed Noise prologue is valid")351}352353struct RejectZeroResolver;354355impl CryptoResolver for RejectZeroResolver {356 fn resolve_rng(&self) -> Option<Box<dyn Random>> {357 DefaultResolver.resolve_rng()358 }359360 fn resolve_dh(&self, choice: &DHChoice) -> Option<Box<dyn Dh>> {361 DefaultResolver362 .resolve_dh(choice)363 .map(|inner| Box::new(RejectZeroDh { inner }) as Box<dyn Dh>)364 }365366 fn resolve_hash(&self, choice: &HashChoice) -> Option<Box<dyn Hash>> {367 DefaultResolver.resolve_hash(choice)368 }369370 fn resolve_cipher(&self, choice: &CipherChoice) -> Option<Box<dyn Cipher>> {371 DefaultResolver.resolve_cipher(choice)372 }373}374375struct RejectZeroDh {376 inner: Box<dyn Dh>,377}378379380381382383const X25519_SMALL_ORDER_PUBKEYS: [[u8; 32]; 9] = [384 [385 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,386 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,387 0x00, 0x00,388 ],389 [390 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,391 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,392 0x00, 0x00,393 ],394 [395 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,396 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,397 0xb8, 0x80,398 ],399 [400 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,401 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,402 0x11, 0x57,403 ],404 [405 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,406 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,407 0xff, 0x7f,408 ],409 [410 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,411 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,412 0xff, 0x7f,413 ],414 [415 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,416 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,417 0xff, 0x7f,418 ],419 [420 0xcd, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,421 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,422 0xb8, 0x80,423 ],424 [425 0x4c, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,426 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,427 0x11, 0xd7,428 ],429];430431fn is_small_order_x25519_pubkey(public_key: &[u8]) -> bool {432 433 434 435 436 let Some(key) = public_key.get(..32) else {437 return false;438 };439 let Ok(key) = <[u8; 32]>::try_from(key) else {440 return false;441 };442 X25519_SMALL_ORDER_PUBKEYS443 .iter()444 .any(|low| low.ct_eq(&key).unwrap_u8() == 1)445}446447impl Dh for RejectZeroDh {448 fn name(&self) -> &'static str {449 self.inner.name()450 }451452 fn pub_len(&self) -> usize {453 self.inner.pub_len()454 }455456 fn priv_len(&self) -> usize {457 self.inner.priv_len()458 }459460 fn set(&mut self, private_key: &[u8]) {461 self.inner.set(private_key);462 }463464 fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {465 self.inner.generate(rng)466 }467468 fn pubkey(&self) -> &[u8] {469 self.inner.pubkey()470 }471472 fn privkey(&self) -> &[u8] {473 self.inner.privkey()474 }475476 fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {477 if is_small_order_x25519_pubkey(public_key) {478 return Err(Error::Dh);479 }480 self.inner.dh(public_key, output)?;481 let used = self.inner.dh_len();482 if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {483 output[..used].zeroize();484 return Err(Error::Dh);485 }486 Ok(())487 }488489 fn dh_len(&self) -> usize {490 self.inner.dh_len()491 }492}493494#[cfg(target_arch = "wasm32")]495mod wasm {496 use super::CoreNoiseSession;497 use wasm_bindgen::prelude::*;498499 fn js_error(message: String) -> JsError {500 JsError::new(&message)501 }502503 #[wasm_bindgen]504 pub struct WasmNoiseSession {505 inner: CoreNoiseSession,506 }507508 #[wasm_bindgen]509 impl WasmNoiseSession {510 #[wasm_bindgen(js_name = nkInitiator)]511 pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {512 Ok(Self {513 inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,514 })515 }516517 #[wasm_bindgen(js_name = nkResponder)]518 pub fn nk_responder(519 local_private_key: &[u8],520 local_public_key: &[u8],521 ) -> Result<WasmNoiseSession, JsError> {522 Ok(Self {523 inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)524 .map_err(js_error)?,525 })526 }527528 #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]529 pub fn nk_initiator_with_ephemeral_for_test(530 remote_public_key: &[u8],531 ephemeral_private_key: &[u8],532 ) -> Result<WasmNoiseSession, JsError> {533 Ok(Self {534 inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(535 remote_public_key,536 ephemeral_private_key,537 )538 .map_err(js_error)?,539 })540 }541542 #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]543 pub fn nk_responder_with_ephemeral_for_test(544 local_private_key: &[u8],545 local_public_key: &[u8],546 ephemeral_private_key: &[u8],547 ) -> Result<WasmNoiseSession, JsError> {548 Ok(Self {549 inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(550 local_private_key,551 local_public_key,552 ephemeral_private_key,553 )554 .map_err(js_error)?,555 })556 }557558 #[wasm_bindgen(js_name = kkInitiator)]559 pub fn kk_initiator(560 local_private_key: &[u8],561 local_public_key: &[u8],562 remote_public_key: &[u8],563 ) -> Result<WasmNoiseSession, JsError> {564 Ok(Self {565 inner: CoreNoiseSession::kk_initiator(566 local_private_key,567 local_public_key,568 remote_public_key,569 )570 .map_err(js_error)?,571 })572 }573574 #[wasm_bindgen(js_name = kkResponder)]575 pub fn kk_responder(576 local_private_key: &[u8],577 local_public_key: &[u8],578 remote_public_key: &[u8],579 ) -> Result<WasmNoiseSession, JsError> {580 Ok(Self {581 inner: CoreNoiseSession::kk_responder(582 local_private_key,583 local_public_key,584 remote_public_key,585 )586 .map_err(js_error)?,587 })588 }589590 #[wasm_bindgen(js_name = kkInitiatorWithEphemeralForTest)]591 pub fn kk_initiator_with_ephemeral_for_test(592 local_private_key: &[u8],593 local_public_key: &[u8],594 remote_public_key: &[u8],595 ephemeral_private_key: &[u8],596 ) -> Result<WasmNoiseSession, JsError> {597 Ok(Self {598 inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(599 local_private_key,600 local_public_key,601 remote_public_key,602 ephemeral_private_key,603 )604 .map_err(js_error)?,605 })606 }607608 #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]609 pub fn kk_responder_with_ephemeral_for_test(610 local_private_key: &[u8],611 local_public_key: &[u8],612 remote_public_key: &[u8],613 ephemeral_private_key: &[u8],614 ) -> Result<WasmNoiseSession, JsError> {615 Ok(Self {616 inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(617 local_private_key,618 local_public_key,619 remote_public_key,620 ephemeral_private_key,621 )622 .map_err(js_error)?,623 })624 }625626 #[wasm_bindgen(js_name = writeHandshake)]627 pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {628 self.inner.write_handshake(payload).map_err(js_error)629 }630631 #[wasm_bindgen(js_name = readHandshake)]632 pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {633 self.inner.read_handshake(message).map_err(js_error)634 }635636 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {637 self.inner.encrypt(plaintext).map_err(js_error)638 }639640 pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {641 self.inner.decrypt(ciphertext).map_err(js_error)642 }643644 #[wasm_bindgen(getter, js_name = handshakeHash)]645 pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {646 self.inner.handshake_hash().map_err(js_error)647 }648649 #[wasm_bindgen(js_name = rekeyOutgoing)]650 pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {651 self.inner.rekey_outgoing().map_err(js_error)652 }653654 #[wasm_bindgen(js_name = rekeyIncoming)]655 pub fn rekey_incoming(&mut self) -> Result<(), JsError> {656 self.inner.rekey_incoming().map_err(js_error)657 }658 }659}660661#[cfg(not(target_arch = "wasm32"))]662mod jni_bridge;663664#[cfg(test)]665mod tests {666 use super::{667 CoreNoiseSession, KK, MAX_NOISE_MESSAGE, NK, NOISE_PROLOGUE, RejectZeroResolver,668 X25519_SMALL_ORDER_PUBKEYS, is_small_order_x25519_pubkey,669 };670 use blake2::{671 Blake2sMac256,672 digest::{KeyInit as BlakeKeyInit, Mac},673 };674 use chacha20poly1305::{675 ChaCha20Poly1305, Nonce,676 aead::{Aead, Payload},677 };678 use snow::{Builder, params::NoiseParams};679 use x25519_dalek::{PublicKey, StaticSecret};680681 fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {682 let private = [seed; 32];683 let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();684 (private, public)685 }686687 fn hex(value: &str) -> Vec<u8> {688 assert!(value.len().is_multiple_of(2));689 value690 .as_bytes()691 .chunks_exact(2)692 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())693 .collect()694 }695696 fn official_pair(697 pattern: &str,698 initiator_static: Option<&str>,699 initiator_ephemeral: &str,700 initiator_remote_static: &str,701 responder_static: &str,702 responder_ephemeral: &str,703 responder_remote_static: Option<&str>,704 ) -> (CoreNoiseSession, CoreNoiseSession) {705 let prologue = hex("4a6f686e2047616c74");706 let responder_private = hex(responder_static);707 let responder_public = PublicKey::from(&StaticSecret::from(708 <[u8; 32]>::try_from(&responder_private[..]).unwrap(),709 ));710 assert_eq!(711 responder_public.as_bytes(),712 &hex(initiator_remote_static)[..]713 );714715 let params: NoiseParams = pattern.parse().unwrap();716 let initiator_ephemeral = hex(initiator_ephemeral);717 let initiator_static = initiator_static.map(hex);718 let responder_ephemeral = hex(responder_ephemeral);719 let responder_remote_static = responder_remote_static.map(hex);720 let mut initiator_builder =721 Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))722 .prologue(&prologue)723 .unwrap()724 .remote_public_key(responder_public.as_bytes())725 .unwrap()726 .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);727 if let Some(private) = &initiator_static {728 initiator_builder = initiator_builder.local_private_key(private).unwrap();729 }730731 let mut responder_builder = Builder::with_resolver(params, Box::new(RejectZeroResolver))732 .prologue(&prologue)733 .unwrap()734 .local_private_key(&responder_private)735 .unwrap()736 .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);737 if let Some(remote_public) = &responder_remote_static {738 responder_builder = responder_builder.remote_public_key(remote_public).unwrap();739 }740741 (742 CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),743 CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),744 )745 }746747 fn assert_cacophony_vector(748 mut initiator: CoreNoiseSession,749 mut responder: CoreNoiseSession,750 handshake_hash: &str,751 messages: &[(&str, &str)],752 ) {753 for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {754 let plaintext = hex(payload);755 let expected = hex(expected_ciphertext);756 if index < 2 {757 let (sender, receiver) = if index % 2 == 0 {758 (&mut initiator, &mut responder)759 } else {760 (&mut responder, &mut initiator)761 };762 let actual = sender.write_handshake(&plaintext).unwrap();763 assert_eq!(actual, expected, "handshake message {index}");764 assert_eq!(765 receiver.read_handshake(&actual).unwrap(),766 plaintext,767 "handshake payload {index}",768 );769 } else {770 let (sender, receiver) = if index % 2 == 0 {771 (&mut initiator, &mut responder)772 } else {773 (&mut responder, &mut initiator)774 };775 let actual = sender.encrypt(&plaintext).unwrap();776 assert_eq!(actual, expected, "transport message {index}");777 assert_eq!(778 receiver.decrypt(&actual).unwrap(),779 plaintext,780 "transport payload {index}",781 );782 }783 }784 let expected_hash = hex(handshake_hash);785 assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);786 assert_eq!(responder.handshake_hash().unwrap(), expected_hash);787 }788789 fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {790 let message1 = initiator.write_handshake(&[]).unwrap();791 assert!(responder.read_handshake(&message1).unwrap().is_empty());792 let message2 = responder.write_handshake(&[]).unwrap();793 assert!(initiator.read_handshake(&message2).unwrap().is_empty());794 assert_eq!(795 initiator.handshake_hash().unwrap(),796 responder.handshake_hash().unwrap()797 );798 }799800 #[test]801 fn mismatched_prologue_rejects_handshake() {802 let (private, _) = keypair(7);803 let remote = keypair(8).1;804 let params: NoiseParams = NK.parse().unwrap();805 let mut initiator = Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))806 .prologue(b"peach-sync-old")807 .unwrap()808 .remote_public_key(&remote)809 .unwrap()810 .fixed_ephemeral_key_for_testing_only(&[1; 32])811 .build_initiator()812 .unwrap();813 let mut responder = Builder::with_resolver(params, Box::new(RejectZeroResolver))814 .prologue(NOISE_PROLOGUE)815 .unwrap()816 .local_private_key(&private)817 .unwrap()818 .fixed_ephemeral_key_for_testing_only(&[2; 32])819 .build_responder()820 .unwrap();821 let mut message = [0_u8; MAX_NOISE_MESSAGE];822 let size = initiator.write_message(&[], &mut message).unwrap();823 assert!(responder.read_message(&message[..size], &mut []).is_err());824 }825826 #[test]827 fn nk_round_trip_uses_one_state_machine() {828 let (responder_private, responder_public) = keypair(7);829 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();830 let mut responder =831 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();832 complete(&mut initiator, &mut responder);833834 let ciphertext = initiator.encrypt(b"peach").unwrap();835 assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");836 let response = responder.encrypt(b"sync").unwrap();837 assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");838 }839840 841 842 843 844 845 #[test]846 fn small_order_remote_static_fails_handshake() {847 let (local_private, local_public) = keypair(11);848 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {849 let mut initiator =850 CoreNoiseSession::kk_initiator(&local_private, &local_public, low).unwrap();851 assert!(852 initiator.write_handshake(&[]).is_err(),853 "handshake against small-order static must fail"854 );855 assert!(is_small_order_x25519_pubkey(low));856 }857 let (_, real_public) = keypair(9);858 assert!(!is_small_order_x25519_pubkey(&real_public));859 }860861 862 863 864 #[test]865 fn small_order_guard_handles_snow_padded_buffers() {866 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {867 let mut padded = [0u8; 56];868 padded[..32].copy_from_slice(low);869 assert!(is_small_order_x25519_pubkey(&padded));870 }871 let (_, real_public) = keypair(13);872 let mut padded = [0u8; 56];873 padded[..32].copy_from_slice(&real_public);874 assert!(!is_small_order_x25519_pubkey(&padded));875 }876877 #[test]878 fn kk_round_trip_authenticates_both_static_keys() {879 let (initiator_private, initiator_public) = keypair(11);880 let (responder_private, responder_public) = keypair(19);881 let mut initiator = CoreNoiseSession::kk_initiator(882 &initiator_private,883 &initiator_public,884 &responder_public,885 )886 .unwrap();887 let mut responder = CoreNoiseSession::kk_responder(888 &responder_private,889 &responder_public,890 &initiator_public,891 )892 .unwrap();893 complete(&mut initiator, &mut responder);894 }895896 #[test]897 fn mismatched_local_keypair_is_rejected() {898 let (private, _) = keypair(2);899 let (_, wrong_public) = keypair(3);900 let result = CoreNoiseSession::nk_responder(&private, &wrong_public);901 assert!(matches!(result, Err(error) if error.contains("does not match")));902 }903904 #[test]905 fn tampered_transport_frame_is_rejected() {906 let (responder_private, responder_public) = keypair(23);907 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();908 let mut responder =909 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();910 complete(&mut initiator, &mut responder);911 let mut ciphertext = initiator.encrypt(b"vault").unwrap();912 ciphertext[0] ^= 0x80;913 assert!(responder.decrypt(&ciphertext).is_err());914 }915916 #[test]917 fn cacophony_nk_vector_matches_shared_core() {918 let pair = official_pair(919 NK,920 None,921 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",922 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",923 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",924 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",925 None,926 );927 assert_cacophony_vector(928 pair.0,929 pair.1,930 "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",931 &[932 (933 "4c756477696720766f6e204d69736573",934 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",935 ),936 (937 "4d757272617920526f746862617264",938 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",939 ),940 (941 "462e20412e20486179656b",942 "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",943 ),944 (945 "4361726c204d656e676572",946 "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",947 ),948 ],949 );950 }951952 #[test]953 fn cacophony_kk_vector_matches_shared_core() {954 let pair = official_pair(955 KK,956 Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),957 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",958 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",959 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",960 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",961 Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),962 );963 assert_cacophony_vector(964 pair.0,965 pair.1,966 "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",967 &[968 (969 "4c756477696720766f6e204d69736573",970 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",971 ),972 (973 "4d757272617920526f746862617264",974 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",975 ),976 (977 "462e20412e20486179656b",978 "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",979 ),980 (981 "4361726c204d656e676572",982 "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",983 ),984 ],985 );986 }987988 #[test]989 fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {990 let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");991 let nonce = hex("070000004041424344454647");992 let aad = hex("50515253c0c1c2c3c4c5c6c7");993 let plaintext = hex(concat!(994 "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",995 "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",996 "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",997 "637265656e20776f756c642062652069742e",998 ));999 let expected = hex(concat!(1000 "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",1001 "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",1002 "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",1003 "3ff4def08e4b7a9de576d26586cec64b6116",1004 "1ae10b594f09e26a7e902ecbd0600691",1005 ));1006 let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();1007 assert_eq!(1008 cipher1009 .encrypt(1010 Nonce::from_slice(&nonce),1011 Payload {1012 msg: &plaintext,1013 aad: &aad,1014 },1015 )1016 .unwrap(),1017 expected,1018 );1019 }10201021 #[test]1022 fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {1023 let key: Vec<u8> = (0_u8..32).collect();1024 let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();1025 Mac::update(&mut mac, &[]);1026 assert_eq!(1027 Mac::finalize(mac).into_bytes().as_slice(),1028 &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),1029 );1030 }1031}