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 340 341 342 let private = Zeroizing::new(<[u8; 32]>::try_from(private_key).expect("length checked"));343 let derived = PublicKey::from(&StaticSecret::from(*private));344 if derived.as_bytes().ct_eq(public_key).unwrap_u8() != 1 {345 return Err("local public key does not match local private key".into());346 }347 Ok(())348}349350fn display_error(error: impl std::fmt::Display) -> String {351 error.to_string()352}353354fn core_builder(params: NoiseParams) -> Builder<'static> {355 Builder::with_resolver(params, Box::new(RejectZeroResolver))356 .prologue(NOISE_PROLOGUE)357 .expect("fixed Noise prologue is valid")358}359360struct RejectZeroResolver;361362impl CryptoResolver for RejectZeroResolver {363 fn resolve_rng(&self) -> Option<Box<dyn Random>> {364 DefaultResolver.resolve_rng()365 }366367 fn resolve_dh(&self, choice: &DHChoice) -> Option<Box<dyn Dh>> {368 DefaultResolver369 .resolve_dh(choice)370 .map(|inner| Box::new(RejectZeroDh { inner }) as Box<dyn Dh>)371 }372373 fn resolve_hash(&self, choice: &HashChoice) -> Option<Box<dyn Hash>> {374 DefaultResolver.resolve_hash(choice)375 }376377 fn resolve_cipher(&self, choice: &CipherChoice) -> Option<Box<dyn Cipher>> {378 DefaultResolver.resolve_cipher(choice)379 }380}381382struct RejectZeroDh {383 inner: Box<dyn Dh>,384}385386387388389390const X25519_SMALL_ORDER_PUBKEYS: [[u8; 32]; 9] = [391 [392 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,393 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,394 0x00, 0x00,395 ],396 [397 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,398 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,399 0x00, 0x00,400 ],401 [402 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,403 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,404 0xb8, 0x80,405 ],406 [407 0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,408 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,409 0x11, 0x57,410 ],411 [412 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,413 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,414 0xff, 0x7f,415 ],416 [417 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,418 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,419 0xff, 0x7f,420 ],421 [422 0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,423 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,424 0xff, 0x7f,425 ],426 [427 0xcd, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,428 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,429 0xb8, 0x80,430 ],431 [432 0x4c, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,433 0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,434 0x11, 0xd7,435 ],436];437438fn is_small_order_x25519_pubkey(public_key: &[u8]) -> bool {439 440 441 442 443 let Some(key) = public_key.get(..32) else {444 return false;445 };446 let Ok(key) = <[u8; 32]>::try_from(key) else {447 return false;448 };449 X25519_SMALL_ORDER_PUBKEYS450 .iter()451 .any(|low| low.ct_eq(&key).unwrap_u8() == 1)452}453454impl Dh for RejectZeroDh {455 fn name(&self) -> &'static str {456 self.inner.name()457 }458459 fn pub_len(&self) -> usize {460 self.inner.pub_len()461 }462463 fn priv_len(&self) -> usize {464 self.inner.priv_len()465 }466467 fn set(&mut self, private_key: &[u8]) {468 self.inner.set(private_key);469 }470471 fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {472 self.inner.generate(rng)473 }474475 fn pubkey(&self) -> &[u8] {476 self.inner.pubkey()477 }478479 fn privkey(&self) -> &[u8] {480 self.inner.privkey()481 }482483 fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {484 if is_small_order_x25519_pubkey(public_key) {485 return Err(Error::Dh);486 }487 self.inner.dh(public_key, output)?;488 let used = self.inner.dh_len();489 if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {490 output[..used].zeroize();491 return Err(Error::Dh);492 }493 Ok(())494 }495496 fn dh_len(&self) -> usize {497 self.inner.dh_len()498 }499}500501#[cfg(target_arch = "wasm32")]502mod wasm {503 use super::CoreNoiseSession;504 use wasm_bindgen::prelude::*;505506 fn js_error(message: String) -> JsError {507 JsError::new(&message)508 }509510 #[wasm_bindgen]511 pub struct WasmNoiseSession {512 inner: CoreNoiseSession,513 }514515 #[wasm_bindgen]516 impl WasmNoiseSession {517 #[wasm_bindgen(js_name = nkInitiator)]518 pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {519 Ok(Self {520 inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,521 })522 }523524 #[wasm_bindgen(js_name = nkResponder)]525 pub fn nk_responder(526 local_private_key: &[u8],527 local_public_key: &[u8],528 ) -> Result<WasmNoiseSession, JsError> {529 Ok(Self {530 inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)531 .map_err(js_error)?,532 })533 }534535 #[cfg(feature = "test-constructors")]536 #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]537 pub fn nk_initiator_with_ephemeral_for_test(538 remote_public_key: &[u8],539 ephemeral_private_key: &[u8],540 ) -> Result<WasmNoiseSession, JsError> {541 Ok(Self {542 inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(543 remote_public_key,544 ephemeral_private_key,545 )546 .map_err(js_error)?,547 })548 }549550 #[cfg(feature = "test-constructors")]551 #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]552 pub fn nk_responder_with_ephemeral_for_test(553 local_private_key: &[u8],554 local_public_key: &[u8],555 ephemeral_private_key: &[u8],556 ) -> Result<WasmNoiseSession, JsError> {557 Ok(Self {558 inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(559 local_private_key,560 local_public_key,561 ephemeral_private_key,562 )563 .map_err(js_error)?,564 })565 }566567 #[wasm_bindgen(js_name = kkInitiator)]568 pub fn kk_initiator(569 local_private_key: &[u8],570 local_public_key: &[u8],571 remote_public_key: &[u8],572 ) -> Result<WasmNoiseSession, JsError> {573 Ok(Self {574 inner: CoreNoiseSession::kk_initiator(575 local_private_key,576 local_public_key,577 remote_public_key,578 )579 .map_err(js_error)?,580 })581 }582583 #[wasm_bindgen(js_name = kkResponder)]584 pub fn kk_responder(585 local_private_key: &[u8],586 local_public_key: &[u8],587 remote_public_key: &[u8],588 ) -> Result<WasmNoiseSession, JsError> {589 Ok(Self {590 inner: CoreNoiseSession::kk_responder(591 local_private_key,592 local_public_key,593 remote_public_key,594 )595 .map_err(js_error)?,596 })597 }598599 #[cfg(feature = "test-constructors")]600 #[wasm_bindgen(js_name = kkInitiatorWithEphemeralForTest)]601 pub fn kk_initiator_with_ephemeral_for_test(602 local_private_key: &[u8],603 local_public_key: &[u8],604 remote_public_key: &[u8],605 ephemeral_private_key: &[u8],606 ) -> Result<WasmNoiseSession, JsError> {607 Ok(Self {608 inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(609 local_private_key,610 local_public_key,611 remote_public_key,612 ephemeral_private_key,613 )614 .map_err(js_error)?,615 })616 }617618 #[cfg(feature = "test-constructors")]619 #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]620 pub fn kk_responder_with_ephemeral_for_test(621 local_private_key: &[u8],622 local_public_key: &[u8],623 remote_public_key: &[u8],624 ephemeral_private_key: &[u8],625 ) -> Result<WasmNoiseSession, JsError> {626 Ok(Self {627 inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(628 local_private_key,629 local_public_key,630 remote_public_key,631 ephemeral_private_key,632 )633 .map_err(js_error)?,634 })635 }636637 #[wasm_bindgen(js_name = writeHandshake)]638 pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {639 self.inner.write_handshake(payload).map_err(js_error)640 }641642 #[wasm_bindgen(js_name = readHandshake)]643 pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {644 self.inner.read_handshake(message).map_err(js_error)645 }646647 pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {648 self.inner.encrypt(plaintext).map_err(js_error)649 }650651 pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {652 self.inner.decrypt(ciphertext).map_err(js_error)653 }654655 #[wasm_bindgen(getter, js_name = handshakeHash)]656 pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {657 self.inner.handshake_hash().map_err(js_error)658 }659660 #[wasm_bindgen(js_name = rekeyOutgoing)]661 pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {662 self.inner.rekey_outgoing().map_err(js_error)663 }664665 #[wasm_bindgen(js_name = rekeyIncoming)]666 pub fn rekey_incoming(&mut self) -> Result<(), JsError> {667 self.inner.rekey_incoming().map_err(js_error)668 }669 }670}671672#[cfg(not(target_arch = "wasm32"))]673mod jni_bridge;674675#[cfg(test)]676mod tests {677 use super::{678 CoreNoiseSession, KK, MAX_NOISE_MESSAGE, NK, NOISE_PROLOGUE, RejectZeroResolver,679 X25519_SMALL_ORDER_PUBKEYS, is_small_order_x25519_pubkey,680 };681 use blake2::{682 Blake2sMac256,683 digest::{KeyInit as BlakeKeyInit, Mac},684 };685 use chacha20poly1305::{686 ChaCha20Poly1305, Nonce,687 aead::{Aead, Payload},688 };689 use snow::{Builder, params::NoiseParams};690 use x25519_dalek::{PublicKey, StaticSecret};691692 fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {693 let private = [seed; 32];694 let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();695 (private, public)696 }697698 fn hex(value: &str) -> Vec<u8> {699 assert!(value.len().is_multiple_of(2));700 value701 .as_bytes()702 .chunks_exact(2)703 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())704 .collect()705 }706707 fn official_pair(708 pattern: &str,709 initiator_static: Option<&str>,710 initiator_ephemeral: &str,711 initiator_remote_static: &str,712 responder_static: &str,713 responder_ephemeral: &str,714 responder_remote_static: Option<&str>,715 ) -> (CoreNoiseSession, CoreNoiseSession) {716 let prologue = hex("4a6f686e2047616c74");717 let responder_private = hex(responder_static);718 let responder_public = PublicKey::from(&StaticSecret::from(719 <[u8; 32]>::try_from(&responder_private[..]).unwrap(),720 ));721 assert_eq!(722 responder_public.as_bytes(),723 &hex(initiator_remote_static)[..]724 );725726 let params: NoiseParams = pattern.parse().unwrap();727 let initiator_ephemeral = hex(initiator_ephemeral);728 let initiator_static = initiator_static.map(hex);729 let responder_ephemeral = hex(responder_ephemeral);730 let responder_remote_static = responder_remote_static.map(hex);731 let mut initiator_builder =732 Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))733 .prologue(&prologue)734 .unwrap()735 .remote_public_key(responder_public.as_bytes())736 .unwrap()737 .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);738 if let Some(private) = &initiator_static {739 initiator_builder = initiator_builder.local_private_key(private).unwrap();740 }741742 let mut responder_builder = Builder::with_resolver(params, Box::new(RejectZeroResolver))743 .prologue(&prologue)744 .unwrap()745 .local_private_key(&responder_private)746 .unwrap()747 .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);748 if let Some(remote_public) = &responder_remote_static {749 responder_builder = responder_builder.remote_public_key(remote_public).unwrap();750 }751752 (753 CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),754 CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),755 )756 }757758 fn assert_cacophony_vector(759 mut initiator: CoreNoiseSession,760 mut responder: CoreNoiseSession,761 handshake_hash: &str,762 messages: &[(&str, &str)],763 ) {764 for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {765 let plaintext = hex(payload);766 let expected = hex(expected_ciphertext);767 if index < 2 {768 let (sender, receiver) = if index % 2 == 0 {769 (&mut initiator, &mut responder)770 } else {771 (&mut responder, &mut initiator)772 };773 let actual = sender.write_handshake(&plaintext).unwrap();774 assert_eq!(actual, expected, "handshake message {index}");775 assert_eq!(776 receiver.read_handshake(&actual).unwrap(),777 plaintext,778 "handshake payload {index}",779 );780 } else {781 let (sender, receiver) = if index % 2 == 0 {782 (&mut initiator, &mut responder)783 } else {784 (&mut responder, &mut initiator)785 };786 let actual = sender.encrypt(&plaintext).unwrap();787 assert_eq!(actual, expected, "transport message {index}");788 assert_eq!(789 receiver.decrypt(&actual).unwrap(),790 plaintext,791 "transport payload {index}",792 );793 }794 }795 let expected_hash = hex(handshake_hash);796 assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);797 assert_eq!(responder.handshake_hash().unwrap(), expected_hash);798 }799800 fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {801 let message1 = initiator.write_handshake(&[]).unwrap();802 assert!(responder.read_handshake(&message1).unwrap().is_empty());803 let message2 = responder.write_handshake(&[]).unwrap();804 assert!(initiator.read_handshake(&message2).unwrap().is_empty());805 assert_eq!(806 initiator.handshake_hash().unwrap(),807 responder.handshake_hash().unwrap()808 );809 }810811 #[test]812 fn mismatched_prologue_rejects_handshake() {813 let (private, _) = keypair(7);814 let remote = keypair(8).1;815 let params: NoiseParams = NK.parse().unwrap();816 let mut initiator = Builder::with_resolver(params.clone(), Box::new(RejectZeroResolver))817 .prologue(b"peach-sync-old")818 .unwrap()819 .remote_public_key(&remote)820 .unwrap()821 .fixed_ephemeral_key_for_testing_only(&[1; 32])822 .build_initiator()823 .unwrap();824 let mut responder = Builder::with_resolver(params, Box::new(RejectZeroResolver))825 .prologue(NOISE_PROLOGUE)826 .unwrap()827 .local_private_key(&private)828 .unwrap()829 .fixed_ephemeral_key_for_testing_only(&[2; 32])830 .build_responder()831 .unwrap();832 let mut message = [0_u8; MAX_NOISE_MESSAGE];833 let size = initiator.write_message(&[], &mut message).unwrap();834 assert!(responder.read_message(&message[..size], &mut []).is_err());835 }836837 #[test]838 fn nk_round_trip_uses_one_state_machine() {839 let (responder_private, responder_public) = keypair(7);840 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();841 let mut responder =842 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();843 complete(&mut initiator, &mut responder);844845 let ciphertext = initiator.encrypt(b"peach").unwrap();846 assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");847 let response = responder.encrypt(b"sync").unwrap();848 assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");849 }850851 852 853 854 855 856 #[test]857 fn small_order_remote_static_fails_handshake() {858 let (local_private, local_public) = keypair(11);859 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {860 let mut initiator =861 CoreNoiseSession::kk_initiator(&local_private, &local_public, low).unwrap();862 assert!(863 initiator.write_handshake(&[]).is_err(),864 "handshake against small-order static must fail"865 );866 assert!(is_small_order_x25519_pubkey(low));867 }868 let (_, real_public) = keypair(9);869 assert!(!is_small_order_x25519_pubkey(&real_public));870 }871872 873 874 875 #[test]876 fn small_order_guard_handles_snow_padded_buffers() {877 for low in X25519_SMALL_ORDER_PUBKEYS.iter() {878 let mut padded = [0u8; 56];879 padded[..32].copy_from_slice(low);880 assert!(is_small_order_x25519_pubkey(&padded));881 }882 let (_, real_public) = keypair(13);883 let mut padded = [0u8; 56];884 padded[..32].copy_from_slice(&real_public);885 assert!(!is_small_order_x25519_pubkey(&padded));886 }887888 #[test]889 fn kk_round_trip_authenticates_both_static_keys() {890 let (initiator_private, initiator_public) = keypair(11);891 let (responder_private, responder_public) = keypair(19);892 let mut initiator = CoreNoiseSession::kk_initiator(893 &initiator_private,894 &initiator_public,895 &responder_public,896 )897 .unwrap();898 let mut responder = CoreNoiseSession::kk_responder(899 &responder_private,900 &responder_public,901 &initiator_public,902 )903 .unwrap();904 complete(&mut initiator, &mut responder);905 }906907 #[test]908 fn mismatched_local_keypair_is_rejected() {909 let (private, _) = keypair(2);910 let (_, wrong_public) = keypair(3);911 let result = CoreNoiseSession::nk_responder(&private, &wrong_public);912 assert!(matches!(result, Err(error) if error.contains("does not match")));913 }914915 #[test]916 fn tampered_transport_frame_is_rejected() {917 let (responder_private, responder_public) = keypair(23);918 let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();919 let mut responder =920 CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();921 complete(&mut initiator, &mut responder);922 let mut ciphertext = initiator.encrypt(b"vault").unwrap();923 ciphertext[0] ^= 0x80;924 assert!(responder.decrypt(&ciphertext).is_err());925 }926927 #[test]928 fn cacophony_nk_vector_matches_shared_core() {929 let pair = official_pair(930 NK,931 None,932 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",933 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",934 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",935 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",936 None,937 );938 assert_cacophony_vector(939 pair.0,940 pair.1,941 "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",942 &[943 (944 "4c756477696720766f6e204d69736573",945 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",946 ),947 (948 "4d757272617920526f746862617264",949 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",950 ),951 (952 "462e20412e20486179656b",953 "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",954 ),955 (956 "4361726c204d656e676572",957 "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",958 ),959 ],960 );961 }962963 #[test]964 fn cacophony_kk_vector_matches_shared_core() {965 let pair = official_pair(966 KK,967 Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),968 "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",969 "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",970 "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",971 "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",972 Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),973 );974 assert_cacophony_vector(975 pair.0,976 pair.1,977 "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",978 &[979 (980 "4c756477696720766f6e204d69736573",981 "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",982 ),983 (984 "4d757272617920526f746862617264",985 "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",986 ),987 (988 "462e20412e20486179656b",989 "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",990 ),991 (992 "4361726c204d656e676572",993 "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",994 ),995 ],996 );997 }998999 #[test]1000 fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {1001 let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");1002 let nonce = hex("070000004041424344454647");1003 let aad = hex("50515253c0c1c2c3c4c5c6c7");1004 let plaintext = hex(concat!(1005 "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",1006 "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",1007 "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",1008 "637265656e20776f756c642062652069742e",1009 ));1010 let expected = hex(concat!(1011 "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",1012 "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",1013 "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",1014 "3ff4def08e4b7a9de576d26586cec64b6116",1015 "1ae10b594f09e26a7e902ecbd0600691",1016 ));1017 let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();1018 assert_eq!(1019 cipher1020 .encrypt(1021 Nonce::from_slice(&nonce),1022 Payload {1023 msg: &plaintext,1024 aad: &aad,1025 },1026 )1027 .unwrap(),1028 expected,1029 );1030 }10311032 1033 1034 1035 #[test]1036 fn pb_26_608_private_key_copy_is_zeroized() {1037 let source = include_str!("lib.rs");1038 let body = source1039 .split("fn require_matching_keypair")1040 .nth(1)1041 .expect("require_matching_keypair is present");1042 assert!(1043 body.contains("Zeroizing::new"),1044 "the private-key copy must be Zeroizing-wrapped"1045 );1046 }10471048 #[test]1049 fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {1050 let key: Vec<u8> = (0_u8..32).collect();1051 let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();1052 Mac::update(&mut mac, &[]);1053 assert_eq!(1054 Mac::finalize(mac).into_bytes().as_slice(),1055 &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),1056 );1057 }1058}