lib.rs

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