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<Box<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(Box::new(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.as_mut()),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 326 327 328 329 330 331 332 333 if let SessionState::Transport(transport) = &mut self.state {334 transport.rekey_manually(Some(&[0_u8; 32]), Some(&[0_u8; 32]));335 }336 self.state = SessionState::Closed;337 }338}339340fn require_key(key: &[u8], label: &str) -> CoreResult<()> {341 if key.len() != 32 {342 return Err(format!("{label} must be 32 bytes"));343 }344 Ok(())345}346347fn require_matching_keypair(private_key: &[u8], public_key: &[u8]) -> CoreResult<()> {348 require_key(private_key, "local private key")?;349 require_key(public_key, "local public key")?;350 351 352 353 let private = Zeroizing::new(<[u8; 32]>::try_from(private_key).expect("length checked"));354 let derived = PublicKey::from(&StaticSecret::from(*private));355 if derived.as_bytes().ct_eq(public_key).unwrap_u8() != 1 {356 return Err("local public key does not match local private key".into());357 }358 Ok(())359}360361fn display_error(error: impl std::fmt::Display) -> String {362 error.to_string()363}364365fn core_builder(params: NoiseParams) -> Builder<'static> {366 Builder::with_resolver(params, Box::new(RejectZeroResolver))367 .prologue(NOISE_PROLOGUE)368 .expect("fixed Noise prologue is valid")369}370371struct RejectZeroResolver;372373impl CryptoResolver for RejectZeroResolver {374 fn resolve_rng(&self) -> Option<Box<dyn Random>> {375 DefaultResolver.resolve_rng()376 }377378 fn resolve_dh(&self, choice: &DHChoice) -> Option<Box<dyn Dh>> {379 DefaultResolver380 .resolve_dh(choice)381 .map(|inner| Box::new(RejectZeroDh { inner }) as Box<dyn Dh>)382 }383384 fn resolve_hash(&self, choice: &HashChoice) -> Option<Box<dyn Hash>> {385 DefaultResolver.resolve_hash(choice)386 }387388 fn resolve_cipher(&self, choice: &CipherChoice) -> Option<Box<dyn Cipher>> {389 DefaultResolver.resolve_cipher(choice)390 }391}392393struct RejectZeroDh {394 inner: Box<dyn Dh>,395}396397398399400401const X25519_SMALL_ORDER_PUBKEYS: [[u8; 32]; 9] = [402 [403 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,404 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,405 0x00, 0x00,406 ],407 [408 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,409 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,410 0x00, 0x00,411 ],412 [413 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,414 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,415 0xb8, 0x80,416 ],417 [418 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,419 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,420 0x11, 0x57,421 ],422 [423 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,424 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,425 0xff, 0x7f,426 ],427 [428 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,429 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,430 0xff, 0x7f,431 ],432 [433 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,434 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,435 0xff, 0x7f,436 ],437 [438 0xcd, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,439 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,440 0xb8, 0x80,441 ],442 [443 0x4c, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,444 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,445 0x11, 0xd7,446 ],447];448449fn is_small_order_x25519_pubkey(public_key: &[u8]) -> bool {450 451 452 453 454 let Some(key) = public_key.get(..32) else {455 return false;456 };457 let Ok(key) = <[u8; 32]>::try_from(key) else {458 return false;459 };460 X25519_SMALL_ORDER_PUBKEYS461 .iter()462 .any(|low| low.ct_eq(&key).unwrap_u8() == 1)463}464465impl Dh for RejectZeroDh {466 fn name(&self) -> &'static str {467 self.inner.name()468 }469470 fn pub_len(&self) -> usize {471 self.inner.pub_len()472 }473474 fn priv_len(&self) -> usize {475 self.inner.priv_len()476 }477478 fn set(&mut self, private_key: &[u8]) {479 self.inner.set(private_key);480 }481482 fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {483 self.inner.generate(rng)484 }485486 fn pubkey(&self) -> &[u8] {487 self.inner.pubkey()488 }489490 fn privkey(&self) -> &[u8] {491 self.inner.privkey()492 }493494 fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {495 if is_small_order_x25519_pubkey(public_key) {496 return Err(Error::Dh);497 }498 self.inner.dh(public_key, output)?;499 let used = self.inner.dh_len();500 if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {501 output[..used].zeroize();502 return Err(Error::Dh);503 }504 Ok(())505 }506507 fn dh_len(&self) -> usize {508 self.inner.dh_len()509 }510}511512#[cfg(target_arch = "wasm32")]513mod wasm {514 use super::CoreNoiseSession;515 use wasm_bindgen::prelude::*;516517 fn js_error(message: String) -> JsError {518 JsError::new(&message)519 }520521 #[wasm_bindgen]522 pub struct WasmNoiseSession {523 inner: CoreNoiseSession,524 }525526 #[wasm_bindgen]527 impl WasmNoiseSession {528 #[wasm_bindgen(js_name = nkInitiator)]529 pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {530 Ok(Self {531 inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,532 })533 }534535 #[wasm_bindgen(js_name = nkResponder)]536 pub fn nk_responder(537 local_private_key: &[u8],538 local_public_key: &[u8],539 ) -> Result<WasmNoiseSession, JsError> {540 Ok(Self {541 inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)542 .map_err(js_error)?,543 })544 }545546 #[cfg(feature = "test-constructors")]547 #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]548 pub fn nk_initiator_with_ephemeral_for_test(549 remote_public_key: &[u8],550 ephemeral_private_key: &[u8],551 ) -> Result<WasmNoiseSession, JsError> {552 Ok(Self {553 inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(554 remote_public_key,555 ephemeral_private_key,556 )557 .map_err(js_error)?,558 })559 }560561 #[cfg(feature = "test-constructors")]562 #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]563 pub fn nk_responder_with_ephemeral_for_test(564 local_private_key: &[u8],565 local_public_key: &[u8],566 ephemeral_private_key: &[u8],567 ) -> Result<WasmNoiseSession, JsError> {568 Ok(Self {569 inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(570 local_private_key,571 local_public_key,572 ephemeral_private_key,573 )574 .map_err(js_error)?,575 })576 }577578 #[wasm_bindgen(js_name = kkInitiator)]579 pub fn kk_initiator(580 local_private_key: &[u8],581 local_public_key: &[u8],582 remote_public_key: &[u8],583 ) -> Result<WasmNoiseSession, JsError> {584 Ok(Self {585 inner: CoreNoiseSession::kk_initiator(586 local_private_key,587 local_public_key,588 remote_public_key,589 )590 .map_err(js_error)?,591 })592 }593594 #[wasm_bindgen(js_name = kkResponder)]595 pub fn kk_responder(596 local_private_key: &[u8],597 local_public_key: &[u8],598 remote_public_key: &[u8],599 ) -> Result<WasmNoiseSession, JsError> {600 Ok(Self {601 inner: CoreNoiseSession::kk_responder(602 local_private_key,603 local_public_key,604 remote_public_key,605 )606 .map_err(js_error)?,607 })608 }609610 #[cfg(feature = "test-constructors")]611 #[wasm_bindgen(js_name = kkInitiatorWithEphemeralForTest)]612 pub fn kk_initiator_with_ephemeral_for_test(613 local_private_key: &[u8],614 local_public_key: &[u8],615 remote_public_key: &[u8],616 ephemeral_private_key: &[u8],617 ) -> Result<WasmNoiseSession, JsError> {618 Ok(Self {619 inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(620 local_private_key,621 local_public_key,622 remote_public_key,623 ephemeral_private_key,624 )625 .map_err(js_error)?,626 })627 }628629 #[cfg(feature = "test-constructors")]630 #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]631 pub fn kk_responder_with_ephemeral_for_test(632 local_private_key: &[u8],633 local_public_key: &[u8],634 remote_public_key: &[u8],635 ephemeral_private_key: &[u8],636 ) -> Result<WasmNoiseSession, JsError> {637 Ok(Self {638 inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(639 local_private_key,640 local_public_key,641 remote_public_key,642 ephemeral_private_key,643 )644 .map_err(js_error)?,645 })646 }647648 #[wasm_bindgen(js_name = writeHandshake)]649 pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {650 self.inner.write_handshake(payload).map_err(js_error)651 }652653 #[wasm_bindgen(js_name = readHandshake)]654 pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {655 self.inner.read_handshake(message).map_err(js_error)656 }657658 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {659 self.inner.encrypt(plaintext).map_err(js_error)660 }661662 pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {663 self.inner.decrypt(ciphertext).map_err(js_error)664 }665666 #[wasm_bindgen(getter, js_name = handshakeHash)]667 pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {668 self.inner.handshake_hash().map_err(js_error)669 }670671 #[wasm_bindgen(js_name = rekeyOutgoing)]672 pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {673 self.inner.rekey_outgoing().map_err(js_error)674 }675676 #[wasm_bindgen(js_name = rekeyIncoming)]677 pub fn rekey_incoming(&mut self) -> Result<(), JsError> {678 self.inner.rekey_incoming().map_err(js_error)679 }680 }681}682683#[cfg(not(target_arch = "wasm32"))]684mod jni_bridge;685686#[cfg(test)]687mod tests {688 use super::{689 CoreNoiseSession, KK, MAX_NOISE_MESSAGE, NK, NOISE_PROLOGUE, RejectZeroResolver,690 X25519_SMALL_ORDER_PUBKEYS, is_small_order_x25519_pubkey,691 };692 use blake2::{693 Blake2sMac256,694 digest::{KeyInit as BlakeKeyInit, Mac},695 };696 use chacha20poly1305::{697 ChaCha20Poly1305, Nonce,698 aead::{Aead, Payload},699 };700 use snow::{Builder, params::NoiseParams};701 use x25519_dalek::{PublicKey, StaticSecret};702703 fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {704 let private = [seed; 32];705 let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();706 (private, public)707 }708709 fn hex(value: &str) -> Vec<u8> {710 assert!(value.len().is_multiple_of(2));711 value712 .as_bytes()713 .chunks_exact(2)714 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())715 .collect()716 }717718 fn official_pair(719 pattern: &str,720 initiator_static: Option<&str>,721 initiator_ephemeral: &str,722 initiator_remote_static: &str,723 responder_static: &str,724 responder_ephemeral: &str,725 responder_remote_static: Option<&str>,726 ) -> (CoreNoiseSession, CoreNoiseSession) {727 let prologue = hex("4a6f686e2047616c74");728 let responder_private = hex(responder_static);729 let responder_public = PublicKey::from(&StaticSecret::from(730 <[u8; 32]>::try_from(&responder_private[..]).unwrap(),731 ));732 assert_eq!(733 responder_public.as_bytes(),734 &hex(initiator_remote_static)[..]735 );736737 let params: NoiseParams = pattern.parse().unwrap();738 let initiator_ephemeral = hex(initiator_ephemeral);739 let initiator_static = initiator_static.map(hex);740 let responder_ephemeral = hex(responder_ephemeral);741 let responder_remote_static = responder_remote_static.map(hex);742 let mut initiator_builder =743 Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))744 .prologue(&prologue)745 .unwrap()746 .remote_public_key(responder_public.as_bytes())747 .unwrap()748 .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);749 if let Some(private) = &initiator_static {750 initiator_builder = initiator_builder.local_private_key(private).unwrap();751 }752753 let mut responder_builder = Builder::with_resolver(params, Box::new(RejectZeroResolver))754 .prologue(&prologue)755 .unwrap()756 .local_private_key(&responder_private)757 .unwrap()758 .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);759 if let Some(remote_public) = &responder_remote_static {760 responder_builder = responder_builder.remote_public_key(remote_public).unwrap();761 }762763 (764 CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),765 CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),766 )767 }768769 fn assert_cacophony_vector(770 mut initiator: CoreNoiseSession,771 mut responder: CoreNoiseSession,772 handshake_hash: &str,773 messages: &[(&str, &str)],774 ) {775 for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {776 let plaintext = hex(payload);777 let expected = hex(expected_ciphertext);778 if index < 2 {779 let (sender, receiver) = if index % 2 == 0 {780 (&mut initiator, &mut responder)781 } else {782 (&mut responder, &mut initiator)783 };784 let actual = sender.write_handshake(&plaintext).unwrap();785 assert_eq!(actual, expected, "handshake message {index}");786 assert_eq!(787 receiver.read_handshake(&actual).unwrap(),788 plaintext,789 "handshake payload {index}",790 );791 } else {792 let (sender, receiver) = if index % 2 == 0 {793 (&mut initiator, &mut responder)794 } else {795 (&mut responder, &mut initiator)796 };797 let actual = sender.encrypt(&plaintext).unwrap();798 assert_eq!(actual, expected, "transport message {index}");799 assert_eq!(800 receiver.decrypt(&actual).unwrap(),801 plaintext,802 "transport payload {index}",803 );804 }805 }806 let expected_hash = hex(handshake_hash);807 assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);808 assert_eq!(responder.handshake_hash().unwrap(), expected_hash);809 }810811 fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {812 let message1 = initiator.write_handshake(&[]).unwrap();813 assert!(responder.read_handshake(&message1).unwrap().is_empty());814 let message2 = responder.write_handshake(&[]).unwrap();815 assert!(initiator.read_handshake(&message2).unwrap().is_empty());816 assert_eq!(817 initiator.handshake_hash().unwrap(),818 responder.handshake_hash().unwrap()819 );820 }821822 #[test]823 fn mismatched_prologue_rejects_handshake() {824 let (private, _) = keypair(7);825 let remote = keypair(8).1;826 let params: NoiseParams = NK.parse().unwrap();827 let mut initiator = Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))828 .prologue(b"peach-sync-old")829 .unwrap()830 .remote_public_key(&remote)831 .unwrap()832 .fixed_ephemeral_key_for_testing_only(&[1; 32])833 .build_initiator()834 .unwrap();835 let mut responder = Builder::with_resolver(params, Box::new(RejectZeroResolver))836 .prologue(NOISE_PROLOGUE)837 .unwrap()838 .local_private_key(&private)839 .unwrap()840 .fixed_ephemeral_key_for_testing_only(&[2; 32])841 .build_responder()842 .unwrap();843 let mut message = [0_u8; MAX_NOISE_MESSAGE];844 let size = initiator.write_message(&[], &mut message).unwrap();845 assert!(responder.read_message(&message[..size], &mut []).is_err());846 }847848 #[test]849 fn nk_round_trip_uses_one_state_machine() {850 let (responder_private, responder_public) = keypair(7);851 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();852 let mut responder =853 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();854 complete(&mut initiator, &mut responder);855856 let ciphertext = initiator.encrypt(b"peach").unwrap();857 assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");858 let response = responder.encrypt(b"sync").unwrap();859 assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");860 }861862 863 864 865 866 867 #[test]868 fn small_order_remote_static_fails_handshake() {869 let (local_private, local_public) = keypair(11);870 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {871 let mut initiator =872 CoreNoiseSession::kk_initiator(&local_private, &local_public, low).unwrap();873 assert!(874 initiator.write_handshake(&[]).is_err(),875 "handshake against small-order static must fail"876 );877 assert!(is_small_order_x25519_pubkey(low));878 }879 let (_, real_public) = keypair(9);880 assert!(!is_small_order_x25519_pubkey(&real_public));881 }882883 884 885 886 #[test]887 fn small_order_guard_handles_snow_padded_buffers() {888 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {889 let mut padded = [0u8; 56];890 padded[..32].copy_from_slice(low);891 assert!(is_small_order_x25519_pubkey(&padded));892 }893 let (_, real_public) = keypair(13);894 let mut padded = [0u8; 56];895 padded[..32].copy_from_slice(&real_public);896 assert!(!is_small_order_x25519_pubkey(&padded));897 }898899 #[test]900 fn kk_round_trip_authenticates_both_static_keys() {901 let (initiator_private, initiator_public) = keypair(11);902 let (responder_private, responder_public) = keypair(19);903 let mut initiator = CoreNoiseSession::kk_initiator(904 &initiator_private,905 &initiator_public,906 &responder_public,907 )908 .unwrap();909 let mut responder = CoreNoiseSession::kk_responder(910 &responder_private,911 &responder_public,912 &initiator_public,913 )914 .unwrap();915 complete(&mut initiator, &mut responder);916 }917918 #[test]919 fn mismatched_local_keypair_is_rejected() {920 let (private, _) = keypair(2);921 let (_, wrong_public) = keypair(3);922 let result = CoreNoiseSession::nk_responder(&private, &wrong_public);923 assert!(matches!(result, Err(error) if error.contains("does not match")));924 }925926 #[test]927 fn tampered_transport_frame_is_rejected() {928 let (responder_private, responder_public) = keypair(23);929 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();930 let mut responder =931 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();932 complete(&mut initiator, &mut responder);933 let mut ciphertext = initiator.encrypt(b"vault").unwrap();934 ciphertext[0] ^= 0x80;935 assert!(responder.decrypt(&ciphertext).is_err());936 }937938 #[test]939 fn cacophony_nk_vector_matches_shared_core() {940 let pair = official_pair(941 NK,942 None,943 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",944 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",945 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",946 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",947 None,948 );949 assert_cacophony_vector(950 pair.0,951 pair.1,952 "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",953 &[954 (955 "4c756477696720766f6e204d69736573",956 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",957 ),958 (959 "4d757272617920526f746862617264",960 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",961 ),962 (963 "462e20412e20486179656b",964 "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",965 ),966 (967 "4361726c204d656e676572",968 "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",969 ),970 ],971 );972 }973974 #[test]975 fn cacophony_kk_vector_matches_shared_core() {976 let pair = official_pair(977 KK,978 Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),979 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",980 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",981 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",982 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",983 Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),984 );985 assert_cacophony_vector(986 pair.0,987 pair.1,988 "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",989 &[990 (991 "4c756477696720766f6e204d69736573",992 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",993 ),994 (995 "4d757272617920526f746862617264",996 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",997 ),998 (999 "462e20412e20486179656b",1000 "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",1001 ),1002 (1003 "4361726c204d656e676572",1004 "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",1005 ),1006 ],1007 );1008 }10091010 #[test]1011 fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {1012 let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");1013 let nonce = hex("070000004041424344454647");1014 let aad = hex("50515253c0c1c2c3c4c5c6c7");1015 let plaintext = hex(concat!(1016 "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",1017 "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",1018 "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",1019 "637265656e20776f756c642062652069742e",1020 ));1021 let expected = hex(concat!(1022 "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",1023 "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",1024 "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",1025 "3ff4def08e4b7a9de576d26586cec64b6116",1026 "1ae10b594f09e26a7e902ecbd0600691",1027 ));1028 let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();1029 assert_eq!(1030 cipher1031 .encrypt(1032 Nonce::from_slice(&nonce),1033 Payload {1034 msg: &plaintext,1035 aad: &aad,1036 },1037 )1038 .unwrap(),1039 expected,1040 );1041 }10421043 1044 1045 fn source_after(marker: &str) -> String {1046 let source = include_str!("lib.rs");1047 let after = source.split(marker).nth(1).expect("marker is present");1048 let end = after1049 .find("\nfn ")1050 .or_else(|| after.find("\nimpl "))1051 .unwrap_or(after.len());1052 after[..end].to_string()1053 }10541055 1056 1057 1058 #[test]1059 fn pb_26_608_private_key_copy_is_zeroized() {1060 let body = source_after("fn require_matching_keypair");1061 assert!(1062 body.contains("Zeroizing::new"),1063 "the private-key copy must be Zeroizing-wrapped"1064 );1065 }10661067 1068 1069 1070 #[test]1071 fn pb_26_609_zero_rekey_replaces_the_transport_cipher_key() {1072 let params: NoiseParams = NK.parse().unwrap();1073 let responder_static = keypair(9);1074 let mut handshake_initiator = Builder::new(params.clone())1075 .remote_public_key(&responder_static.1)1076 .unwrap()1077 .build_initiator()1078 .unwrap();1079 let mut handshake_responder = Builder::new(params)1080 .local_private_key(&responder_static.0)1081 .unwrap()1082 .build_responder()1083 .unwrap();10841085 let mut wire = [0_u8; 1024];1086 let mut out = [0_u8; 1024];1087 let len = handshake_initiator.write_message(&[], &mut wire).unwrap();1088 handshake_responder1089 .read_message(&wire[..len], &mut out)1090 .unwrap();1091 let len = handshake_responder.write_message(&[], &mut wire).unwrap();1092 handshake_initiator1093 .read_message(&wire[..len], &mut out)1094 .unwrap();10951096 let mut initiator = handshake_initiator1097 .into_transport_mode()1098 .expect("handshake must be finished");1099 let mut responder = handshake_responder1100 .into_transport_mode()1101 .expect("handshake must be finished");11021103 let mut frame = [0_u8; 64];1104 let len = initiator.write_message(b"secret", &mut frame).unwrap();1105 let frame = frame[..len].to_vec();11061107 initiator.rekey_manually(Some(&[0_u8; 32]), Some(&[0_u8; 32]));11081109 1110 1111 assert!(initiator.read_message(&frame, &mut [0_u8; 64]).is_err());1112 1113 1114 let mut next = [0_u8; 64];1115 let len = initiator.write_message(b"after", &mut next).unwrap();1116 assert!(1117 responder1118 .read_message(&next[..len], &mut [0_u8; 64])1119 .is_err()1120 );1121 }11221123 1124 1125 #[test]1126 fn pb_26_609_drop_zeroizes_transport_cipher_keys() {1127 let body = source_after("impl Drop for CoreNoiseSession");1128 assert!(1129 body.contains("rekey_manually"),1130 "drop must overwrite both transport cipher keys before closing"1131 );1132 }11331134 #[test]1135 fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {1136 let key: Vec<u8> = (0_u8..32).collect();1137 let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();1138 Mac::update(&mut mac, &[]);1139 assert_eq!(1140 Mac::finalize(mac).into_bytes().as_slice(),1141 &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),1142 );1143 }1144}