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 #[cfg(feature = "test-constructors")]43 #[doc(hidden)]44 pub fn nk_initiator_with_ephemeral_for_test(45 remote_public_key: &[u8],46 ephemeral_private_key: &[u8],47 ) -> CoreResult<Self> {48 Self::nk_initiator_inner(remote_public_key, Some(ephemeral_private_key))49 }5051 fn nk_initiator_inner(52 remote_public_key: &[u8],53 ephemeral_private_key: Option<&[u8]>,54 ) -> CoreResult<Self> {55 require_key(remote_public_key, "responder public key")?;56 let params: NoiseParams = NK.parse().map_err(display_error)?;57 let mut builder = core_builder(params)58 .remote_public_key(remote_public_key)59 .map_err(display_error)?;60 if let Some(ephemeral) = ephemeral_private_key {61 require_key(ephemeral, "initiator ephemeral private key")?;62 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);63 }64 let handshake = builder.build_initiator().map_err(display_error)?;65 Ok(Self::handshake(handshake))66 }6768 pub fn nk_responder(local_private_key: &[u8], local_public_key: &[u8]) -> CoreResult<Self> {69 Self::nk_responder_inner(local_private_key, local_public_key, None)70 }7172 #[cfg(feature = "test-constructors")]73 #[doc(hidden)]74 pub fn nk_responder_with_ephemeral_for_test(75 local_private_key: &[u8],76 local_public_key: &[u8],77 ephemeral_private_key: &[u8],78 ) -> CoreResult<Self> {79 Self::nk_responder_inner(80 local_private_key,81 local_public_key,82 Some(ephemeral_private_key),83 )84 }8586 fn nk_responder_inner(87 local_private_key: &[u8],88 local_public_key: &[u8],89 ephemeral_private_key: Option<&[u8]>,90 ) -> CoreResult<Self> {91 require_matching_keypair(local_private_key, local_public_key)?;92 let private = Zeroizing::new(local_private_key.to_vec());93 let params: NoiseParams = NK.parse().map_err(display_error)?;94 let mut builder = core_builder(params)95 .local_private_key(private.as_slice())96 .map_err(display_error)?;97 if let Some(ephemeral) = ephemeral_private_key {98 require_key(ephemeral, "responder ephemeral private key")?;99 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);100 }101 let handshake = builder.build_responder().map_err(display_error)?;102 Ok(Self::handshake(handshake))103 }104105 pub fn kk_initiator(106 local_private_key: &[u8],107 local_public_key: &[u8],108 remote_public_key: &[u8],109 ) -> CoreResult<Self> {110 Self::kk_initiator_inner(local_private_key, local_public_key, remote_public_key, None)111 }112113 #[cfg(feature = "test-constructors")]114 #[doc(hidden)]115 pub fn kk_initiator_with_ephemeral_for_test(116 local_private_key: &[u8],117 local_public_key: &[u8],118 remote_public_key: &[u8],119 ephemeral_private_key: &[u8],120 ) -> CoreResult<Self> {121 Self::kk_initiator_inner(122 local_private_key,123 local_public_key,124 remote_public_key,125 Some(ephemeral_private_key),126 )127 }128129 fn kk_initiator_inner(130 local_private_key: &[u8],131 local_public_key: &[u8],132 remote_public_key: &[u8],133 ephemeral_private_key: Option<&[u8]>,134 ) -> CoreResult<Self> {135 require_matching_keypair(local_private_key, local_public_key)?;136 require_key(remote_public_key, "responder public key")?;137 let private = Zeroizing::new(local_private_key.to_vec());138 let params: NoiseParams = KK.parse().map_err(display_error)?;139 let mut builder = core_builder(params)140 .local_private_key(private.as_slice())141 .map_err(display_error)?142 .remote_public_key(remote_public_key)143 .map_err(display_error)?;144 if let Some(ephemeral) = ephemeral_private_key {145 require_key(ephemeral, "initiator ephemeral private key")?;146 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);147 }148 let handshake = builder.build_initiator().map_err(display_error)?;149 Ok(Self::handshake(handshake))150 }151152 pub fn kk_responder(153 local_private_key: &[u8],154 local_public_key: &[u8],155 remote_public_key: &[u8],156 ) -> CoreResult<Self> {157 Self::kk_responder_inner(local_private_key, local_public_key, remote_public_key, None)158 }159160 #[cfg(feature = "test-constructors")]161 #[doc(hidden)]162 pub fn kk_responder_with_ephemeral_for_test(163 local_private_key: &[u8],164 local_public_key: &[u8],165 remote_public_key: &[u8],166 ephemeral_private_key: &[u8],167 ) -> CoreResult<Self> {168 Self::kk_responder_inner(169 local_private_key,170 local_public_key,171 remote_public_key,172 Some(ephemeral_private_key),173 )174 }175176 fn kk_responder_inner(177 local_private_key: &[u8],178 local_public_key: &[u8],179 remote_public_key: &[u8],180 ephemeral_private_key: Option<&[u8]>,181 ) -> CoreResult<Self> {182 require_matching_keypair(local_private_key, local_public_key)?;183 require_key(remote_public_key, "initiator public key")?;184 let private = Zeroizing::new(local_private_key.to_vec());185 let params: NoiseParams = KK.parse().map_err(display_error)?;186 let mut builder = core_builder(params)187 .local_private_key(private.as_slice())188 .map_err(display_error)?189 .remote_public_key(remote_public_key)190 .map_err(display_error)?;191 if let Some(ephemeral) = ephemeral_private_key {192 require_key(ephemeral, "responder ephemeral private key")?;193 builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);194 }195 let handshake = builder.build_responder().map_err(display_error)?;196 Ok(Self::handshake(handshake))197 }198199 fn handshake(handshake: HandshakeState) -> Self {200 Self {201 state: SessionState::Handshake(Some(handshake)),202 handshake_hash: None,203 }204 }205206 pub fn write_handshake(&mut self, payload: &[u8]) -> CoreResult<Vec<u8>> {207 let handshake = self.handshake_mut()?;208 if !handshake.is_my_turn() {209 return Err("Noise handshake write attempted out of turn".into());210 }211 let mut output = vec![0_u8; MAX_NOISE_MESSAGE];212 let size = handshake213 .write_message(payload, &mut output)214 .map_err(display_error)?;215 output.truncate(size);216 self.finish_if_ready()?;217 Ok(output)218 }219220 pub fn read_handshake(&mut self, message: &[u8]) -> CoreResult<Vec<u8>> {221 if message.len() > MAX_NOISE_MESSAGE {222 return Err("Noise handshake message exceeds 65535 bytes".into());223 }224 let handshake = self.handshake_mut()?;225 if handshake.is_my_turn() {226 return Err("Noise handshake read attempted out of turn".into());227 }228 let mut payload = vec![0_u8; MAX_NOISE_MESSAGE];229 let size = handshake230 .read_message(message, &mut payload)231 .map_err(display_error)?;232 payload.truncate(size);233 self.finish_if_ready()?;234 Ok(payload)235 }236237 pub fn encrypt(&mut self, plaintext: &[u8]) -> CoreResult<Vec<u8>> {238 if plaintext.len() > MAX_NOISE_MESSAGE - TAG_LEN {239 return Err("Noise plaintext exceeds transport limit".into());240 }241 let transport = self.transport_mut()?;242 let mut output = vec![0_u8; plaintext.len() + TAG_LEN];243 let size = transport244 .write_message(plaintext, &mut output)245 .map_err(display_error)?;246 output.truncate(size);247 Ok(output)248 }249250 pub fn decrypt(&mut self, ciphertext: &[u8]) -> CoreResult<Vec<u8>> {251 if ciphertext.len() > MAX_NOISE_MESSAGE {252 return Err("Noise ciphertext exceeds transport limit".into());253 }254 let transport = self.transport_mut()?;255 let mut output = vec![0_u8; ciphertext.len()];256 let size = transport257 .read_message(ciphertext, &mut output)258 .map_err(display_error)?;259 output.truncate(size);260 Ok(output)261 }262263 pub fn handshake_hash(&self) -> CoreResult<Vec<u8>> {264 self.handshake_hash265 .map(|hash| hash.to_vec())266 .ok_or_else(|| "Noise handshake is not complete".into())267 }268269 pub fn rekey_outgoing(&mut self) -> CoreResult<()> {270 self.transport_mut()?.rekey_outgoing();271 Ok(())272 }273274 pub fn rekey_incoming(&mut self) -> CoreResult<()> {275 self.transport_mut()?.rekey_incoming();276 Ok(())277 }278279 fn handshake_mut(&mut self) -> CoreResult<&mut HandshakeState> {280 match &mut self.state {281 SessionState::Handshake(Some(handshake)) => Ok(handshake),282 SessionState::Handshake(None) => Err("Noise handshake is transitioning".into()),283 SessionState::Transport(_) => Err("Noise handshake is already complete".into()),284 SessionState::Closed => Err("Noise session is closed".into()),285 }286 }287288 fn transport_mut(&mut self) -> CoreResult<&mut TransportState> {289 match &mut self.state {290 SessionState::Transport(transport) => Ok(transport),291 SessionState::Handshake(_) => Err("Noise handshake is not complete".into()),292 SessionState::Closed => Err("Noise session is closed".into()),293 }294 }295296 fn finish_if_ready(&mut self) -> CoreResult<()> {297 let ready = matches!(298 &self.state,299 SessionState::Handshake(Some(handshake)) if handshake.is_handshake_finished()300 );301 if !ready {302 return Ok(());303 }304305 let handshake = match std::mem::replace(&mut self.state, SessionState::Closed) {306 SessionState::Handshake(Some(handshake)) => handshake,307 _ => return Err("Noise session transition failed".into()),308 };309 let hash: [u8; HASH_LEN] = handshake310 .get_handshake_hash()311 .try_into()312 .map_err(|_| "Noise handshake hash must be 32 bytes".to_string())?;313 let transport = handshake.into_transport_mode().map_err(display_error)?;314 self.handshake_hash = Some(hash);315 self.state = SessionState::Transport(transport);316 Ok(())317 }318}319320impl Drop for CoreNoiseSession {321 fn drop(&mut self) {322 if let Some(hash) = &mut self.handshake_hash {323 hash.zeroize();324 }325 self.state = SessionState::Closed;326 }327}328329fn require_key(key: &[u8], label: &str) -> CoreResult<()> {330 if key.len() != 32 {331 return Err(format!("{label} must be 32 bytes"));332 }333 Ok(())334}335336fn require_matching_keypair(private_key: &[u8], public_key: &[u8]) -> CoreResult<()> {337 require_key(private_key, "local private key")?;338 require_key(public_key, "local public key")?;339 let private: [u8; 32] = private_key.try_into().expect("length checked");340 let derived = PublicKey::from(&StaticSecret::from(private));341 if derived.as_bytes().ct_eq(public_key).unwrap_u8() != 1 {342 return Err("local public key does not match local private key".into());343 }344 Ok(())345}346347fn display_error(error: impl std::fmt::Display) -> String {348 error.to_string()349}350351fn core_builder(params: NoiseParams) -> Builder<'static> {352 Builder::with_resolver(params, Box::new(RejectZeroResolver))353 .prologue(NOISE_PROLOGUE)354 .expect("fixed Noise prologue is valid")355}356357struct RejectZeroResolver;358359impl CryptoResolver for RejectZeroResolver {360 fn resolve_rng(&self) -> Option<Box<dyn Random>> {361 DefaultResolver.resolve_rng()362 }363364 fn resolve_dh(&self, choice: &DHChoice) -> Option<Box<dyn Dh>> {365 DefaultResolver366 .resolve_dh(choice)367 .map(|inner| Box::new(RejectZeroDh { inner }) as Box<dyn Dh>)368 }369370 fn resolve_hash(&self, choice: &HashChoice) -> Option<Box<dyn Hash>> {371 DefaultResolver.resolve_hash(choice)372 }373374 fn resolve_cipher(&self, choice: &CipherChoice) -> Option<Box<dyn Cipher>> {375 DefaultResolver.resolve_cipher(choice)376 }377}378379struct RejectZeroDh {380 inner: Box<dyn Dh>,381}382383384385386387const X25519_SMALL_ORDER_PUBKEYS: [[u8; 32]; 9] = [388 [389 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,390 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,391 0x00, 0x00,392 ],393 [394 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,395 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,396 0x00, 0x00,397 ],398 [399 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,400 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,401 0xb8, 0x80,402 ],403 [404 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,405 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,406 0x11, 0x57,407 ],408 [409 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,410 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,411 0xff, 0x7f,412 ],413 [414 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,415 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,416 0xff, 0x7f,417 ],418 [419 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,420 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,421 0xff, 0x7f,422 ],423 [424 0xcd, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,425 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,426 0xb8, 0x80,427 ],428 [429 0x4c, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,430 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,431 0x11, 0xd7,432 ],433];434435fn is_small_order_x25519_pubkey(public_key: &[u8]) -> bool {436 437 438 439 440 let Some(key) = public_key.get(..32) else {441 return false;442 };443 let Ok(key) = <[u8; 32]>::try_from(key) else {444 return false;445 };446 X25519_SMALL_ORDER_PUBKEYS447 .iter()448 .any(|low| low.ct_eq(&key).unwrap_u8() == 1)449}450451impl Dh for RejectZeroDh {452 fn name(&self) -> &'static str {453 self.inner.name()454 }455456 fn pub_len(&self) -> usize {457 self.inner.pub_len()458 }459460 fn priv_len(&self) -> usize {461 self.inner.priv_len()462 }463464 fn set(&mut self, private_key: &[u8]) {465 self.inner.set(private_key);466 }467468 fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {469 self.inner.generate(rng)470 }471472 fn pubkey(&self) -> &[u8] {473 self.inner.pubkey()474 }475476 fn privkey(&self) -> &[u8] {477 self.inner.privkey()478 }479480 fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {481 if is_small_order_x25519_pubkey(public_key) {482 return Err(Error::Dh);483 }484 self.inner.dh(public_key, output)?;485 let used = self.inner.dh_len();486 if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {487 output[..used].zeroize();488 return Err(Error::Dh);489 }490 Ok(())491 }492493 fn dh_len(&self) -> usize {494 self.inner.dh_len()495 }496}497498#[cfg(target_arch = "wasm32")]499mod wasm {500 use super::CoreNoiseSession;501 use wasm_bindgen::prelude::*;502503 fn js_error(message: String) -> JsError {504 JsError::new(&message)505 }506507 #[wasm_bindgen]508 pub struct WasmNoiseSession {509 inner: CoreNoiseSession,510 }511512 #[wasm_bindgen]513 impl WasmNoiseSession {514 #[wasm_bindgen(js_name = nkInitiator)]515 pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {516 Ok(Self {517 inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,518 })519 }520521 #[wasm_bindgen(js_name = nkResponder)]522 pub fn nk_responder(523 local_private_key: &[u8],524 local_public_key: &[u8],525 ) -> Result<WasmNoiseSession, JsError> {526 Ok(Self {527 inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)528 .map_err(js_error)?,529 })530 }531532 #[cfg(feature = "test-constructors")]533 #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]534 pub fn nk_initiator_with_ephemeral_for_test(535 remote_public_key: &[u8],536 ephemeral_private_key: &[u8],537 ) -> Result<WasmNoiseSession, JsError> {538 Ok(Self {539 inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(540 remote_public_key,541 ephemeral_private_key,542 )543 .map_err(js_error)?,544 })545 }546547 #[cfg(feature = "test-constructors")]548 #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]549 pub fn nk_responder_with_ephemeral_for_test(550 local_private_key: &[u8],551 local_public_key: &[u8],552 ephemeral_private_key: &[u8],553 ) -> Result<WasmNoiseSession, JsError> {554 Ok(Self {555 inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(556 local_private_key,557 local_public_key,558 ephemeral_private_key,559 )560 .map_err(js_error)?,561 })562 }563564 #[wasm_bindgen(js_name = kkInitiator)]565 pub fn kk_initiator(566 local_private_key: &[u8],567 local_public_key: &[u8],568 remote_public_key: &[u8],569 ) -> Result<WasmNoiseSession, JsError> {570 Ok(Self {571 inner: CoreNoiseSession::kk_initiator(572 local_private_key,573 local_public_key,574 remote_public_key,575 )576 .map_err(js_error)?,577 })578 }579580 #[wasm_bindgen(js_name = kkResponder)]581 pub fn kk_responder(582 local_private_key: &[u8],583 local_public_key: &[u8],584 remote_public_key: &[u8],585 ) -> Result<WasmNoiseSession, JsError> {586 Ok(Self {587 inner: CoreNoiseSession::kk_responder(588 local_private_key,589 local_public_key,590 remote_public_key,591 )592 .map_err(js_error)?,593 })594 }595596 #[cfg(feature = "test-constructors")]597 #[wasm_bindgen(js_name = kkInitiatorWithEphemeralForTest)]598 pub fn kk_initiator_with_ephemeral_for_test(599 local_private_key: &[u8],600 local_public_key: &[u8],601 remote_public_key: &[u8],602 ephemeral_private_key: &[u8],603 ) -> Result<WasmNoiseSession, JsError> {604 Ok(Self {605 inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(606 local_private_key,607 local_public_key,608 remote_public_key,609 ephemeral_private_key,610 )611 .map_err(js_error)?,612 })613 }614615 #[cfg(feature = "test-constructors")]616 #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]617 pub fn kk_responder_with_ephemeral_for_test(618 local_private_key: &[u8],619 local_public_key: &[u8],620 remote_public_key: &[u8],621 ephemeral_private_key: &[u8],622 ) -> Result<WasmNoiseSession, JsError> {623 Ok(Self {624 inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(625 local_private_key,626 local_public_key,627 remote_public_key,628 ephemeral_private_key,629 )630 .map_err(js_error)?,631 })632 }633634 #[wasm_bindgen(js_name = writeHandshake)]635 pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {636 self.inner.write_handshake(payload).map_err(js_error)637 }638639 #[wasm_bindgen(js_name = readHandshake)]640 pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {641 self.inner.read_handshake(message).map_err(js_error)642 }643644 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {645 self.inner.encrypt(plaintext).map_err(js_error)646 }647648 pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {649 self.inner.decrypt(ciphertext).map_err(js_error)650 }651652 #[wasm_bindgen(getter, js_name = handshakeHash)]653 pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {654 self.inner.handshake_hash().map_err(js_error)655 }656657 #[wasm_bindgen(js_name = rekeyOutgoing)]658 pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {659 self.inner.rekey_outgoing().map_err(js_error)660 }661662 #[wasm_bindgen(js_name = rekeyIncoming)]663 pub fn rekey_incoming(&mut self) -> Result<(), JsError> {664 self.inner.rekey_incoming().map_err(js_error)665 }666 }667}668669#[cfg(not(target_arch = "wasm32"))]670mod jni_bridge;671672#[cfg(test)]673mod tests {674 use super::{675 CoreNoiseSession, KK, MAX_NOISE_MESSAGE, NK, NOISE_PROLOGUE, RejectZeroResolver,676 X25519_SMALL_ORDER_PUBKEYS, is_small_order_x25519_pubkey,677 };678 use blake2::{679 Blake2sMac256,680 digest::{KeyInit as BlakeKeyInit, Mac},681 };682 use chacha20poly1305::{683 ChaCha20Poly1305, Nonce,684 aead::{Aead, Payload},685 };686 use snow::{Builder, params::NoiseParams};687 use x25519_dalek::{PublicKey, StaticSecret};688689 fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {690 let private = [seed; 32];691 let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();692 (private, public)693 }694695 fn hex(value: &str) -> Vec<u8> {696 assert!(value.len().is_multiple_of(2));697 value698 .as_bytes()699 .chunks_exact(2)700 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())701 .collect()702 }703704 fn official_pair(705 pattern: &str,706 initiator_static: Option<&str>,707 initiator_ephemeral: &str,708 initiator_remote_static: &str,709 responder_static: &str,710 responder_ephemeral: &str,711 responder_remote_static: Option<&str>,712 ) -> (CoreNoiseSession, CoreNoiseSession) {713 let prologue = hex("4a6f686e2047616c74");714 let responder_private = hex(responder_static);715 let responder_public = PublicKey::from(&StaticSecret::from(716 <[u8; 32]>::try_from(&responder_private[..]).unwrap(),717 ));718 assert_eq!(719 responder_public.as_bytes(),720 &hex(initiator_remote_static)[..]721 );722723 let params: NoiseParams = pattern.parse().unwrap();724 let initiator_ephemeral = hex(initiator_ephemeral);725 let initiator_static = initiator_static.map(hex);726 let responder_ephemeral = hex(responder_ephemeral);727 let responder_remote_static = responder_remote_static.map(hex);728 let mut initiator_builder =729 Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))730 .prologue(&prologue)731 .unwrap()732 .remote_public_key(responder_public.as_bytes())733 .unwrap()734 .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);735 if let Some(private) = &initiator_static {736 initiator_builder = initiator_builder.local_private_key(private).unwrap();737 }738739 let mut responder_builder = Builder::with_resolver(params, Box::new(RejectZeroResolver))740 .prologue(&prologue)741 .unwrap()742 .local_private_key(&responder_private)743 .unwrap()744 .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);745 if let Some(remote_public) = &responder_remote_static {746 responder_builder = responder_builder.remote_public_key(remote_public).unwrap();747 }748749 (750 CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),751 CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),752 )753 }754755 fn assert_cacophony_vector(756 mut initiator: CoreNoiseSession,757 mut responder: CoreNoiseSession,758 handshake_hash: &str,759 messages: &[(&str, &str)],760 ) {761 for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {762 let plaintext = hex(payload);763 let expected = hex(expected_ciphertext);764 if index < 2 {765 let (sender, receiver) = if index % 2 == 0 {766 (&mut initiator, &mut responder)767 } else {768 (&mut responder, &mut initiator)769 };770 let actual = sender.write_handshake(&plaintext).unwrap();771 assert_eq!(actual, expected, "handshake message {index}");772 assert_eq!(773 receiver.read_handshake(&actual).unwrap(),774 plaintext,775 "handshake payload {index}",776 );777 } else {778 let (sender, receiver) = if index % 2 == 0 {779 (&mut initiator, &mut responder)780 } else {781 (&mut responder, &mut initiator)782 };783 let actual = sender.encrypt(&plaintext).unwrap();784 assert_eq!(actual, expected, "transport message {index}");785 assert_eq!(786 receiver.decrypt(&actual).unwrap(),787 plaintext,788 "transport payload {index}",789 );790 }791 }792 let expected_hash = hex(handshake_hash);793 assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);794 assert_eq!(responder.handshake_hash().unwrap(), expected_hash);795 }796797 fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {798 let message1 = initiator.write_handshake(&[]).unwrap();799 assert!(responder.read_handshake(&message1).unwrap().is_empty());800 let message2 = responder.write_handshake(&[]).unwrap();801 assert!(initiator.read_handshake(&message2).unwrap().is_empty());802 assert_eq!(803 initiator.handshake_hash().unwrap(),804 responder.handshake_hash().unwrap()805 );806 }807808 #[test]809 fn mismatched_prologue_rejects_handshake() {810 let (private, _) = keypair(7);811 let remote = keypair(8).1;812 let params: NoiseParams = NK.parse().unwrap();813 let mut initiator = Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))814 .prologue(b"peach-sync-old")815 .unwrap()816 .remote_public_key(&remote)817 .unwrap()818 .fixed_ephemeral_key_for_testing_only(&[1; 32])819 .build_initiator()820 .unwrap();821 let mut responder = Builder::with_resolver(params, Box::new(RejectZeroResolver))822 .prologue(NOISE_PROLOGUE)823 .unwrap()824 .local_private_key(&private)825 .unwrap()826 .fixed_ephemeral_key_for_testing_only(&[2; 32])827 .build_responder()828 .unwrap();829 let mut message = [0_u8; MAX_NOISE_MESSAGE];830 let size = initiator.write_message(&[], &mut message).unwrap();831 assert!(responder.read_message(&message[..size], &mut []).is_err());832 }833834 #[test]835 fn nk_round_trip_uses_one_state_machine() {836 let (responder_private, responder_public) = keypair(7);837 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();838 let mut responder =839 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();840 complete(&mut initiator, &mut responder);841842 let ciphertext = initiator.encrypt(b"peach").unwrap();843 assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");844 let response = responder.encrypt(b"sync").unwrap();845 assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");846 }847848 849 850 851 852 853 #[test]854 fn small_order_remote_static_fails_handshake() {855 let (local_private, local_public) = keypair(11);856 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {857 let mut initiator =858 CoreNoiseSession::kk_initiator(&local_private, &local_public, low).unwrap();859 assert!(860 initiator.write_handshake(&[]).is_err(),861 "handshake against small-order static must fail"862 );863 assert!(is_small_order_x25519_pubkey(low));864 }865 let (_, real_public) = keypair(9);866 assert!(!is_small_order_x25519_pubkey(&real_public));867 }868869 870 871 872 #[test]873 fn small_order_guard_handles_snow_padded_buffers() {874 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {875 let mut padded = [0u8; 56];876 padded[..32].copy_from_slice(low);877 assert!(is_small_order_x25519_pubkey(&padded));878 }879 let (_, real_public) = keypair(13);880 let mut padded = [0u8; 56];881 padded[..32].copy_from_slice(&real_public);882 assert!(!is_small_order_x25519_pubkey(&padded));883 }884885 #[test]886 fn kk_round_trip_authenticates_both_static_keys() {887 let (initiator_private, initiator_public) = keypair(11);888 let (responder_private, responder_public) = keypair(19);889 let mut initiator = CoreNoiseSession::kk_initiator(890 &initiator_private,891 &initiator_public,892 &responder_public,893 )894 .unwrap();895 let mut responder = CoreNoiseSession::kk_responder(896 &responder_private,897 &responder_public,898 &initiator_public,899 )900 .unwrap();901 complete(&mut initiator, &mut responder);902 }903904 #[test]905 fn mismatched_local_keypair_is_rejected() {906 let (private, _) = keypair(2);907 let (_, wrong_public) = keypair(3);908 let result = CoreNoiseSession::nk_responder(&private, &wrong_public);909 assert!(matches!(result, Err(error) if error.contains("does not match")));910 }911912 #[test]913 fn tampered_transport_frame_is_rejected() {914 let (responder_private, responder_public) = keypair(23);915 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();916 let mut responder =917 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();918 complete(&mut initiator, &mut responder);919 let mut ciphertext = initiator.encrypt(b"vault").unwrap();920 ciphertext[0] ^= 0x80;921 assert!(responder.decrypt(&ciphertext).is_err());922 }923924 #[test]925 fn cacophony_nk_vector_matches_shared_core() {926 let pair = official_pair(927 NK,928 None,929 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",930 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",931 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",932 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",933 None,934 );935 assert_cacophony_vector(936 pair.0,937 pair.1,938 "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",939 &[940 (941 "4c756477696720766f6e204d69736573",942 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",943 ),944 (945 "4d757272617920526f746862617264",946 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",947 ),948 (949 "462e20412e20486179656b",950 "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",951 ),952 (953 "4361726c204d656e676572",954 "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",955 ),956 ],957 );958 }959960 #[test]961 fn cacophony_kk_vector_matches_shared_core() {962 let pair = official_pair(963 KK,964 Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),965 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",966 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",967 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",968 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",969 Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),970 );971 assert_cacophony_vector(972 pair.0,973 pair.1,974 "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",975 &[976 (977 "4c756477696720766f6e204d69736573",978 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",979 ),980 (981 "4d757272617920526f746862617264",982 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",983 ),984 (985 "462e20412e20486179656b",986 "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",987 ),988 (989 "4361726c204d656e676572",990 "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",991 ),992 ],993 );994 }995996 #[test]997 fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {998 let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");999 let nonce = hex("070000004041424344454647");1000 let aad = hex("50515253c0c1c2c3c4c5c6c7");1001 let plaintext = hex(concat!(1002 "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",1003 "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",1004 "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",1005 "637265656e20776f756c642062652069742e",1006 ));1007 let expected = hex(concat!(1008 "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",1009 "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",1010 "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",1011 "3ff4def08e4b7a9de576d26586cec64b6116",1012 "1ae10b594f09e26a7e902ecbd0600691",1013 ));1014 let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();1015 assert_eq!(1016 cipher1017 .encrypt(1018 Nonce::from_slice(&nonce),1019 Payload {1020 msg: &plaintext,1021 aad: &aad,1022 },1023 )1024 .unwrap(),1025 expected,1026 );1027 }10281029 #[test]1030 fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {1031 let key: Vec<u8> = (0_u8..32).collect();1032 let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();1033 Mac::update(&mut mac, &[]);1034 assert_eq!(1035 Mac::finalize(mac).into_bytes().as_slice(),1036 &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),1037 );1038 }1039}