lib.rs

37.9 KB · raw

1//! Canonical Peach Noise state machine.2//!3//! The browser and Android adapters are deliberately thin: all NK/KK4//! transcript processing, DH, AEAD, nonce handling, and transport rekeying5//! happen here through `snow`'s unmodified named patterns.67use 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}382383/// Small-order X25519 u-coordinates (little-endian) — the points of order384/// 1, 2, 4 and 8 (libhydrogen's contributory-behaviour blocklist). A DH385/// against one of these yields a constant, low-entropy shared secret, so386/// they are rejected as inputs outright (PB-26-014).387const 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    // snow 0.10 passes the remote static inside its 56-byte MAXDHLEN buffer437    // (32 key bytes + 24 trailing zeros), so a strict 32-byte try_from always438    // fails and the guard would never fire on a real handshake. Compare the439    // leading 32 bytes whenever a full key is present.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    // PB-26-014: contributory behaviour — a small-order remote static must849    // fail the handshake at the DH step instead of contributing a constant850    // low-entropy secret to the keying material. Drive ALL nine entries:851    // seven have an all-zero DH output (the pre-existing check catches them)852    // but two produce non-zero output and only this guard rejects them.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    // snow hands the guard the remote static inside its 56-byte MAXDHLEN870    // buffer (32 key bytes + 24 trailing zeros) — the guard must compare the871    // leading 32 bytes, not require exactly 32 (PB-26-014 regression).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}