lib.rs

36.3 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";19const HASH_LEN: usize = 32;20const TAG_LEN: usize = 16;21const MAX_NOISE_MESSAGE: usize = 65_535;2223type CoreResult<T> = Result<T, String>;2425enum SessionState {26    Handshake(Option<HandshakeState>),27    Transport(TransportState),28    Closed,29}3031pub struct CoreNoiseSession {32    state: SessionState,33    handshake_hash: Option<[u8; HASH_LEN]>,34}3536impl CoreNoiseSession {37    pub fn nk_initiator(remote_public_key: &[u8]) -> CoreResult<Self> {38        Self::nk_initiator_inner(remote_public_key, None)39    }4041    #[doc(hidden)]42    pub fn nk_initiator_with_ephemeral_for_test(43        remote_public_key: &[u8],44        ephemeral_private_key: &[u8],45    ) -> CoreResult<Self> {46        Self::nk_initiator_inner(remote_public_key, Some(ephemeral_private_key))47    }4849    fn nk_initiator_inner(50        remote_public_key: &[u8],51        ephemeral_private_key: Option<&[u8]>,52    ) -> CoreResult<Self> {53        require_key(remote_public_key, "responder public key")?;54        let params: NoiseParams = NK.parse().map_err(display_error)?;55        let mut builder = core_builder(params)56            .remote_public_key(remote_public_key)57            .map_err(display_error)?;58        if let Some(ephemeral) = ephemeral_private_key {59            require_key(ephemeral, "initiator ephemeral private key")?;60            builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);61        }62        let handshake = builder.build_initiator().map_err(display_error)?;63        Ok(Self::handshake(handshake))64    }6566    pub fn nk_responder(local_private_key: &[u8], local_public_key: &[u8]) -> CoreResult<Self> {67        Self::nk_responder_inner(local_private_key, local_public_key, None)68    }6970    #[doc(hidden)]71    pub fn nk_responder_with_ephemeral_for_test(72        local_private_key: &[u8],73        local_public_key: &[u8],74        ephemeral_private_key: &[u8],75    ) -> CoreResult<Self> {76        Self::nk_responder_inner(77            local_private_key,78            local_public_key,79            Some(ephemeral_private_key),80        )81    }8283    fn nk_responder_inner(84        local_private_key: &[u8],85        local_public_key: &[u8],86        ephemeral_private_key: Option<&[u8]>,87    ) -> CoreResult<Self> {88        require_matching_keypair(local_private_key, local_public_key)?;89        let private = Zeroizing::new(local_private_key.to_vec());90        let params: NoiseParams = NK.parse().map_err(display_error)?;91        let mut builder = core_builder(params)92            .local_private_key(private.as_slice())93            .map_err(display_error)?;94        if let Some(ephemeral) = ephemeral_private_key {95            require_key(ephemeral, "responder ephemeral private key")?;96            builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);97        }98        let handshake = builder.build_responder().map_err(display_error)?;99        Ok(Self::handshake(handshake))100    }101102    pub fn kk_initiator(103        local_private_key: &[u8],104        local_public_key: &[u8],105        remote_public_key: &[u8],106    ) -> CoreResult<Self> {107        Self::kk_initiator_inner(local_private_key, local_public_key, remote_public_key, None)108    }109110    #[doc(hidden)]111    pub fn kk_initiator_with_ephemeral_for_test(112        local_private_key: &[u8],113        local_public_key: &[u8],114        remote_public_key: &[u8],115        ephemeral_private_key: &[u8],116    ) -> CoreResult<Self> {117        Self::kk_initiator_inner(118            local_private_key,119            local_public_key,120            remote_public_key,121            Some(ephemeral_private_key),122        )123    }124125    fn kk_initiator_inner(126        local_private_key: &[u8],127        local_public_key: &[u8],128        remote_public_key: &[u8],129        ephemeral_private_key: Option<&[u8]>,130    ) -> CoreResult<Self> {131        require_matching_keypair(local_private_key, local_public_key)?;132        require_key(remote_public_key, "responder public key")?;133        let private = Zeroizing::new(local_private_key.to_vec());134        let params: NoiseParams = KK.parse().map_err(display_error)?;135        let mut builder = core_builder(params)136            .local_private_key(private.as_slice())137            .map_err(display_error)?138            .remote_public_key(remote_public_key)139            .map_err(display_error)?;140        if let Some(ephemeral) = ephemeral_private_key {141            require_key(ephemeral, "initiator ephemeral private key")?;142            builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);143        }144        let handshake = builder.build_initiator().map_err(display_error)?;145        Ok(Self::handshake(handshake))146    }147148    pub fn kk_responder(149        local_private_key: &[u8],150        local_public_key: &[u8],151        remote_public_key: &[u8],152    ) -> CoreResult<Self> {153        Self::kk_responder_inner(local_private_key, local_public_key, remote_public_key, None)154    }155156    #[doc(hidden)]157    pub fn kk_responder_with_ephemeral_for_test(158        local_private_key: &[u8],159        local_public_key: &[u8],160        remote_public_key: &[u8],161        ephemeral_private_key: &[u8],162    ) -> CoreResult<Self> {163        Self::kk_responder_inner(164            local_private_key,165            local_public_key,166            remote_public_key,167            Some(ephemeral_private_key),168        )169    }170171    fn kk_responder_inner(172        local_private_key: &[u8],173        local_public_key: &[u8],174        remote_public_key: &[u8],175        ephemeral_private_key: Option<&[u8]>,176    ) -> CoreResult<Self> {177        require_matching_keypair(local_private_key, local_public_key)?;178        require_key(remote_public_key, "initiator public key")?;179        let private = Zeroizing::new(local_private_key.to_vec());180        let params: NoiseParams = KK.parse().map_err(display_error)?;181        let mut builder = core_builder(params)182            .local_private_key(private.as_slice())183            .map_err(display_error)?184            .remote_public_key(remote_public_key)185            .map_err(display_error)?;186        if let Some(ephemeral) = ephemeral_private_key {187            require_key(ephemeral, "responder ephemeral private key")?;188            builder = builder.fixed_ephemeral_key_for_testing_only(ephemeral);189        }190        let handshake = builder.build_responder().map_err(display_error)?;191        Ok(Self::handshake(handshake))192    }193194    fn handshake(handshake: HandshakeState) -> Self {195        Self {196            state: SessionState::Handshake(Some(handshake)),197            handshake_hash: None,198        }199    }200201    pub fn write_handshake(&mut self, payload: &[u8]) -> CoreResult<Vec<u8>> {202        let handshake = self.handshake_mut()?;203        if !handshake.is_my_turn() {204            return Err("Noise handshake write attempted out of turn".into());205        }206        let mut output = vec![0_u8; MAX_NOISE_MESSAGE];207        let size = handshake208            .write_message(payload, &mut output)209            .map_err(display_error)?;210        output.truncate(size);211        self.finish_if_ready()?;212        Ok(output)213    }214215    pub fn read_handshake(&mut self, message: &[u8]) -> CoreResult<Vec<u8>> {216        if message.len() > MAX_NOISE_MESSAGE {217            return Err("Noise handshake message exceeds 65535 bytes".into());218        }219        let handshake = self.handshake_mut()?;220        if handshake.is_my_turn() {221            return Err("Noise handshake read attempted out of turn".into());222        }223        let mut payload = vec![0_u8; MAX_NOISE_MESSAGE];224        let size = handshake225            .read_message(message, &mut payload)226            .map_err(display_error)?;227        payload.truncate(size);228        self.finish_if_ready()?;229        Ok(payload)230    }231232    pub fn encrypt(&mut self, plaintext: &[u8]) -> CoreResult<Vec<u8>> {233        if plaintext.len() > MAX_NOISE_MESSAGE - TAG_LEN {234            return Err("Noise plaintext exceeds transport limit".into());235        }236        let transport = self.transport_mut()?;237        let mut output = vec![0_u8; plaintext.len() + TAG_LEN];238        let size = transport239            .write_message(plaintext, &mut output)240            .map_err(display_error)?;241        output.truncate(size);242        Ok(output)243    }244245    pub fn decrypt(&mut self, ciphertext: &[u8]) -> CoreResult<Vec<u8>> {246        if ciphertext.len() > MAX_NOISE_MESSAGE {247            return Err("Noise ciphertext exceeds transport limit".into());248        }249        let transport = self.transport_mut()?;250        let mut output = vec![0_u8; ciphertext.len()];251        let size = transport252            .read_message(ciphertext, &mut output)253            .map_err(display_error)?;254        output.truncate(size);255        Ok(output)256    }257258    pub fn handshake_hash(&self) -> CoreResult<Vec<u8>> {259        self.handshake_hash260            .map(|hash| hash.to_vec())261            .ok_or_else(|| "Noise handshake is not complete".into())262    }263264    pub fn rekey_outgoing(&mut self) -> CoreResult<()> {265        self.transport_mut()?.rekey_outgoing();266        Ok(())267    }268269    pub fn rekey_incoming(&mut self) -> CoreResult<()> {270        self.transport_mut()?.rekey_incoming();271        Ok(())272    }273274    fn handshake_mut(&mut self) -> CoreResult<&mut HandshakeState> {275        match &mut self.state {276            SessionState::Handshake(Some(handshake)) => Ok(handshake),277            SessionState::Handshake(None) => Err("Noise handshake is transitioning".into()),278            SessionState::Transport(_) => Err("Noise handshake is already complete".into()),279            SessionState::Closed => Err("Noise session is closed".into()),280        }281    }282283    fn transport_mut(&mut self) -> CoreResult<&mut TransportState> {284        match &mut self.state {285            SessionState::Transport(transport) => Ok(transport),286            SessionState::Handshake(_) => Err("Noise handshake is not complete".into()),287            SessionState::Closed => Err("Noise session is closed".into()),288        }289    }290291    fn finish_if_ready(&mut self) -> CoreResult<()> {292        let ready = matches!(293            &self.state,294            SessionState::Handshake(Some(handshake)) if handshake.is_handshake_finished()295        );296        if !ready {297            return Ok(());298        }299300        let handshake = match std::mem::replace(&mut self.state, SessionState::Closed) {301            SessionState::Handshake(Some(handshake)) => handshake,302            _ => return Err("Noise session transition failed".into()),303        };304        let hash: [u8; HASH_LEN] = handshake305            .get_handshake_hash()306            .try_into()307            .map_err(|_| "Noise handshake hash must be 32 bytes".to_string())?;308        let transport = handshake.into_transport_mode().map_err(display_error)?;309        self.handshake_hash = Some(hash);310        self.state = SessionState::Transport(transport);311        Ok(())312    }313}314315impl Drop for CoreNoiseSession {316    fn drop(&mut self) {317        if let Some(hash) = &mut self.handshake_hash {318            hash.zeroize();319        }320        self.state = SessionState::Closed;321    }322}323324fn require_key(key: &[u8], label: &str) -> CoreResult<()> {325    if key.len() != 32 {326        return Err(format!("{label} must be 32 bytes"));327    }328    Ok(())329}330331fn require_matching_keypair(private_key: &[u8], public_key: &[u8]) -> CoreResult<()> {332    require_key(private_key, "local private key")?;333    require_key(public_key, "local public key")?;334    let private: [u8; 32] = private_key.try_into().expect("length checked");335    let derived = PublicKey::from(&StaticSecret::from(private));336    if derived.as_bytes().ct_eq(public_key).unwrap_u8() != 1 {337        return Err("local public key does not match local private key".into());338    }339    Ok(())340}341342fn display_error(error: impl std::fmt::Display) -> String {343    error.to_string()344}345346fn core_builder(params: NoiseParams) -> Builder<'static> {347    Builder::with_resolver(params, Box::new(RejectZeroResolver))348}349350struct RejectZeroResolver;351352impl CryptoResolver for RejectZeroResolver {353    fn resolve_rng(&self) -> Option<Box<dyn Random>> {354        DefaultResolver.resolve_rng()355    }356357    fn resolve_dh(&self, choice: &DHChoice) -> Option<Box<dyn Dh>> {358        DefaultResolver359            .resolve_dh(choice)360            .map(|inner| Box::new(RejectZeroDh { inner }) as Box<dyn Dh>)361    }362363    fn resolve_hash(&self, choice: &HashChoice) -> Option<Box<dyn Hash>> {364        DefaultResolver.resolve_hash(choice)365    }366367    fn resolve_cipher(&self, choice: &CipherChoice) -> Option<Box<dyn Cipher>> {368        DefaultResolver.resolve_cipher(choice)369    }370}371372struct RejectZeroDh {373    inner: Box<dyn Dh>,374}375376/// Small-order X25519 u-coordinates (little-endian) — the points of order377/// 1, 2, 4 and 8 (libhydrogen's contributory-behaviour blocklist). A DH378/// against one of these yields a constant, low-entropy shared secret, so379/// they are rejected as inputs outright (PB-26-014).380const X25519_SMALL_ORDER_PUBKEYS: [[u8; 32]; 9] = [381    [382        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,383        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,384        0x00, 0x00,385    ],386    [387        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,388        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,389        0x00, 0x00,390    ],391    [392        0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,393        0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,394        0xb8, 0x80,395    ],396    [397        0x5f, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,398        0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,399        0x11, 0x57,400    ],401    [402        0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,403        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,404        0xff, 0x7f,405    ],406    [407        0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,408        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,409        0xff, 0x7f,410    ],411    [412        0xee, 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        0xcd, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3, 0xfa, 0xf1, 0x9f, 0xc4,418        0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32, 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49,419        0xb8, 0x80,420    ],421    [422        0x4c, 0x9c, 0x95, 0xbc, 0xa3, 0x50, 0x8c, 0x24, 0xb1, 0xd0, 0xb1, 0x55, 0x9c, 0x83, 0xef,423        0x5b, 0x04, 0x44, 0x5c, 0xc4, 0x58, 0x1c, 0x8e, 0x86, 0xd8, 0x22, 0x4e, 0xdd, 0xd0, 0x9f,424        0x11, 0xd7,425    ],426];427428fn is_small_order_x25519_pubkey(public_key: &[u8]) -> bool {429    // snow 0.10 passes the remote static inside its 56-byte MAXDHLEN buffer430    // (32 key bytes + 24 trailing zeros), so a strict 32-byte try_from always431    // fails and the guard would never fire on a real handshake. Compare the432    // leading 32 bytes whenever a full key is present.433    let Some(key) = public_key.get(..32) else {434        return false;435    };436    let Ok(key) = <[u8; 32]>::try_from(key) else {437        return false;438    };439    X25519_SMALL_ORDER_PUBKEYS440        .iter()441        .any(|low| low.ct_eq(&key).unwrap_u8() == 1)442}443444impl Dh for RejectZeroDh {445    fn name(&self) -> &'static str {446        self.inner.name()447    }448449    fn pub_len(&self) -> usize {450        self.inner.pub_len()451    }452453    fn priv_len(&self) -> usize {454        self.inner.priv_len()455    }456457    fn set(&mut self, private_key: &[u8]) {458        self.inner.set(private_key);459    }460461    fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {462        self.inner.generate(rng)463    }464465    fn pubkey(&self) -> &[u8] {466        self.inner.pubkey()467    }468469    fn privkey(&self) -> &[u8] {470        self.inner.privkey()471    }472473    fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {474        if is_small_order_x25519_pubkey(public_key) {475            return Err(Error::Dh);476        }477        self.inner.dh(public_key, output)?;478        let used = self.inner.dh_len();479        if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {480            output[..used].zeroize();481            return Err(Error::Dh);482        }483        Ok(())484    }485486    fn dh_len(&self) -> usize {487        self.inner.dh_len()488    }489}490491#[cfg(target_arch = "wasm32")]492mod wasm {493    use super::CoreNoiseSession;494    use wasm_bindgen::prelude::*;495496    fn js_error(message: String) -> JsError {497        JsError::new(&message)498    }499500    #[wasm_bindgen]501    pub struct WasmNoiseSession {502        inner: CoreNoiseSession,503    }504505    #[wasm_bindgen]506    impl WasmNoiseSession {507        #[wasm_bindgen(js_name = nkInitiator)]508        pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {509            Ok(Self {510                inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,511            })512        }513514        #[wasm_bindgen(js_name = nkResponder)]515        pub fn nk_responder(516            local_private_key: &[u8],517            local_public_key: &[u8],518        ) -> Result<WasmNoiseSession, JsError> {519            Ok(Self {520                inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)521                    .map_err(js_error)?,522            })523        }524525        #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]526        pub fn nk_initiator_with_ephemeral_for_test(527            remote_public_key: &[u8],528            ephemeral_private_key: &[u8],529        ) -> Result<WasmNoiseSession, JsError> {530            Ok(Self {531                inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(532                    remote_public_key,533                    ephemeral_private_key,534                )535                .map_err(js_error)?,536            })537        }538539        #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]540        pub fn nk_responder_with_ephemeral_for_test(541            local_private_key: &[u8],542            local_public_key: &[u8],543            ephemeral_private_key: &[u8],544        ) -> Result<WasmNoiseSession, JsError> {545            Ok(Self {546                inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(547                    local_private_key,548                    local_public_key,549                    ephemeral_private_key,550                )551                .map_err(js_error)?,552            })553        }554555        #[wasm_bindgen(js_name = kkInitiator)]556        pub fn kk_initiator(557            local_private_key: &[u8],558            local_public_key: &[u8],559            remote_public_key: &[u8],560        ) -> Result<WasmNoiseSession, JsError> {561            Ok(Self {562                inner: CoreNoiseSession::kk_initiator(563                    local_private_key,564                    local_public_key,565                    remote_public_key,566                )567                .map_err(js_error)?,568            })569        }570571        #[wasm_bindgen(js_name = kkResponder)]572        pub fn kk_responder(573            local_private_key: &[u8],574            local_public_key: &[u8],575            remote_public_key: &[u8],576        ) -> Result<WasmNoiseSession, JsError> {577            Ok(Self {578                inner: CoreNoiseSession::kk_responder(579                    local_private_key,580                    local_public_key,581                    remote_public_key,582                )583                .map_err(js_error)?,584            })585        }586587        #[wasm_bindgen(js_name = kkInitiatorWithEphemeralForTest)]588        pub fn kk_initiator_with_ephemeral_for_test(589            local_private_key: &[u8],590            local_public_key: &[u8],591            remote_public_key: &[u8],592            ephemeral_private_key: &[u8],593        ) -> Result<WasmNoiseSession, JsError> {594            Ok(Self {595                inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(596                    local_private_key,597                    local_public_key,598                    remote_public_key,599                    ephemeral_private_key,600                )601                .map_err(js_error)?,602            })603        }604605        #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]606        pub fn kk_responder_with_ephemeral_for_test(607            local_private_key: &[u8],608            local_public_key: &[u8],609            remote_public_key: &[u8],610            ephemeral_private_key: &[u8],611        ) -> Result<WasmNoiseSession, JsError> {612            Ok(Self {613                inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(614                    local_private_key,615                    local_public_key,616                    remote_public_key,617                    ephemeral_private_key,618                )619                .map_err(js_error)?,620            })621        }622623        #[wasm_bindgen(js_name = writeHandshake)]624        pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {625            self.inner.write_handshake(payload).map_err(js_error)626        }627628        #[wasm_bindgen(js_name = readHandshake)]629        pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {630            self.inner.read_handshake(message).map_err(js_error)631        }632633        pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {634            self.inner.encrypt(plaintext).map_err(js_error)635        }636637        pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {638            self.inner.decrypt(ciphertext).map_err(js_error)639        }640641        #[wasm_bindgen(getter, js_name = handshakeHash)]642        pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {643            self.inner.handshake_hash().map_err(js_error)644        }645646        #[wasm_bindgen(js_name = rekeyOutgoing)]647        pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {648            self.inner.rekey_outgoing().map_err(js_error)649        }650651        #[wasm_bindgen(js_name = rekeyIncoming)]652        pub fn rekey_incoming(&mut self) -> Result<(), JsError> {653            self.inner.rekey_incoming().map_err(js_error)654        }655    }656}657658#[cfg(not(target_arch = "wasm32"))]659mod jni_bridge;660661#[cfg(test)]662mod tests {663    use super::{664        CoreNoiseSession, KK, NK, X25519_SMALL_ORDER_PUBKEYS, core_builder,665        is_small_order_x25519_pubkey,666    };667    use blake2::{668        Blake2sMac256,669        digest::{KeyInit as BlakeKeyInit, Mac},670    };671    use chacha20poly1305::{672        ChaCha20Poly1305, Nonce,673        aead::{Aead, Payload},674    };675    use snow::params::NoiseParams;676    use x25519_dalek::{PublicKey, StaticSecret};677678    fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {679        let private = [seed; 32];680        let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();681        (private, public)682    }683684    fn hex(value: &str) -> Vec<u8> {685        assert!(value.len().is_multiple_of(2));686        value687            .as_bytes()688            .chunks_exact(2)689            .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())690            .collect()691    }692693    fn official_pair(694        pattern: &str,695        initiator_static: Option<&str>,696        initiator_ephemeral: &str,697        initiator_remote_static: &str,698        responder_static: &str,699        responder_ephemeral: &str,700        responder_remote_static: Option<&str>,701    ) -> (CoreNoiseSession, CoreNoiseSession) {702        let prologue = hex("4a6f686e2047616c74");703        let responder_private = hex(responder_static);704        let responder_public = PublicKey::from(&StaticSecret::from(705            <[u8; 32]>::try_from(&responder_private[..]).unwrap(),706        ));707        assert_eq!(708            responder_public.as_bytes(),709            &hex(initiator_remote_static)[..]710        );711712        let params: NoiseParams = pattern.parse().unwrap();713        let initiator_ephemeral = hex(initiator_ephemeral);714        let initiator_static = initiator_static.map(hex);715        let responder_ephemeral = hex(responder_ephemeral);716        let responder_remote_static = responder_remote_static.map(hex);717        let mut initiator_builder = core_builder(params.clone())718            .prologue(&prologue)719            .unwrap()720            .remote_public_key(responder_public.as_bytes())721            .unwrap()722            .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);723        if let Some(private) = &initiator_static {724            initiator_builder = initiator_builder.local_private_key(private).unwrap();725        }726727        let mut responder_builder = core_builder(params)728            .prologue(&prologue)729            .unwrap()730            .local_private_key(&responder_private)731            .unwrap()732            .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);733        if let Some(remote_public) = &responder_remote_static {734            responder_builder = responder_builder.remote_public_key(remote_public).unwrap();735        }736737        (738            CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),739            CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),740        )741    }742743    fn assert_cacophony_vector(744        mut initiator: CoreNoiseSession,745        mut responder: CoreNoiseSession,746        handshake_hash: &str,747        messages: &[(&str, &str)],748    ) {749        for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {750            let plaintext = hex(payload);751            let expected = hex(expected_ciphertext);752            if index < 2 {753                let (sender, receiver) = if index % 2 == 0 {754                    (&mut initiator, &mut responder)755                } else {756                    (&mut responder, &mut initiator)757                };758                let actual = sender.write_handshake(&plaintext).unwrap();759                assert_eq!(actual, expected, "handshake message {index}");760                assert_eq!(761                    receiver.read_handshake(&actual).unwrap(),762                    plaintext,763                    "handshake payload {index}",764                );765            } else {766                let (sender, receiver) = if index % 2 == 0 {767                    (&mut initiator, &mut responder)768                } else {769                    (&mut responder, &mut initiator)770                };771                let actual = sender.encrypt(&plaintext).unwrap();772                assert_eq!(actual, expected, "transport message {index}");773                assert_eq!(774                    receiver.decrypt(&actual).unwrap(),775                    plaintext,776                    "transport payload {index}",777                );778            }779        }780        let expected_hash = hex(handshake_hash);781        assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);782        assert_eq!(responder.handshake_hash().unwrap(), expected_hash);783    }784785    fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {786        let message1 = initiator.write_handshake(&[]).unwrap();787        assert!(responder.read_handshake(&message1).unwrap().is_empty());788        let message2 = responder.write_handshake(&[]).unwrap();789        assert!(initiator.read_handshake(&message2).unwrap().is_empty());790        assert_eq!(791            initiator.handshake_hash().unwrap(),792            responder.handshake_hash().unwrap()793        );794    }795796    #[test]797    fn nk_round_trip_uses_one_state_machine() {798        let (responder_private, responder_public) = keypair(7);799        let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();800        let mut responder =801            CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();802        complete(&mut initiator, &mut responder);803804        let ciphertext = initiator.encrypt(b"peach").unwrap();805        assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");806        let response = responder.encrypt(b"sync").unwrap();807        assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");808    }809810    // PB-26-014: contributory behaviour — a small-order remote static must811    // fail the handshake at the DH step instead of contributing a constant812    // low-entropy secret to the keying material. Drive ALL nine entries:813    // seven have an all-zero DH output (the pre-existing check catches them)814    // but two produce non-zero output and only this guard rejects them.815    #[test]816    fn small_order_remote_static_fails_handshake() {817        let (local_private, local_public) = keypair(11);818        for low in X25519_SMALL_ORDER_PUBKEYS.iter() {819            let mut initiator =820                CoreNoiseSession::kk_initiator(&local_private, &local_public, low).unwrap();821            assert!(822                initiator.write_handshake(&[]).is_err(),823                "handshake against small-order static must fail"824            );825            assert!(is_small_order_x25519_pubkey(low));826        }827        let (_, real_public) = keypair(9);828        assert!(!is_small_order_x25519_pubkey(&real_public));829    }830831    // snow hands the guard the remote static inside its 56-byte MAXDHLEN832    // buffer (32 key bytes + 24 trailing zeros) — the guard must compare the833    // leading 32 bytes, not require exactly 32 (PB-26-014 regression).834    #[test]835    fn small_order_guard_handles_snow_padded_buffers() {836        for low in X25519_SMALL_ORDER_PUBKEYS.iter() {837            let mut padded = [0u8; 56];838            padded[..32].copy_from_slice(low);839            assert!(is_small_order_x25519_pubkey(&padded));840        }841        let (_, real_public) = keypair(13);842        let mut padded = [0u8; 56];843        padded[..32].copy_from_slice(&real_public);844        assert!(!is_small_order_x25519_pubkey(&padded));845    }846847    #[test]848    fn kk_round_trip_authenticates_both_static_keys() {849        let (initiator_private, initiator_public) = keypair(11);850        let (responder_private, responder_public) = keypair(19);851        let mut initiator = CoreNoiseSession::kk_initiator(852            &initiator_private,853            &initiator_public,854            &responder_public,855        )856        .unwrap();857        let mut responder = CoreNoiseSession::kk_responder(858            &responder_private,859            &responder_public,860            &initiator_public,861        )862        .unwrap();863        complete(&mut initiator, &mut responder);864    }865866    #[test]867    fn mismatched_local_keypair_is_rejected() {868        let (private, _) = keypair(2);869        let (_, wrong_public) = keypair(3);870        let result = CoreNoiseSession::nk_responder(&private, &wrong_public);871        assert!(matches!(result, Err(error) if error.contains("does not match")));872    }873874    #[test]875    fn tampered_transport_frame_is_rejected() {876        let (responder_private, responder_public) = keypair(23);877        let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();878        let mut responder =879            CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();880        complete(&mut initiator, &mut responder);881        let mut ciphertext = initiator.encrypt(b"vault").unwrap();882        ciphertext[0] ^= 0x80;883        assert!(responder.decrypt(&ciphertext).is_err());884    }885886    #[test]887    fn cacophony_nk_vector_matches_shared_core() {888        let pair = official_pair(889            NK,890            None,891            "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",892            "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",893            "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",894            "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",895            None,896        );897        assert_cacophony_vector(898            pair.0,899            pair.1,900            "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",901            &[902                (903                    "4c756477696720766f6e204d69736573",904                    "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",905                ),906                (907                    "4d757272617920526f746862617264",908                    "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",909                ),910                (911                    "462e20412e20486179656b",912                    "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",913                ),914                (915                    "4361726c204d656e676572",916                    "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",917                ),918            ],919        );920    }921922    #[test]923    fn cacophony_kk_vector_matches_shared_core() {924        let pair = official_pair(925            KK,926            Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),927            "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",928            "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",929            "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",930            "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",931            Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),932        );933        assert_cacophony_vector(934            pair.0,935            pair.1,936            "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",937            &[938                (939                    "4c756477696720766f6e204d69736573",940                    "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",941                ),942                (943                    "4d757272617920526f746862617264",944                    "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",945                ),946                (947                    "462e20412e20486179656b",948                    "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",949                ),950                (951                    "4361726c204d656e676572",952                    "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",953                ),954            ],955        );956    }957958    #[test]959    fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {960        let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");961        let nonce = hex("070000004041424344454647");962        let aad = hex("50515253c0c1c2c3c4c5c6c7");963        let plaintext = hex(concat!(964            "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",965            "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",966            "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",967            "637265656e20776f756c642062652069742e",968        ));969        let expected = hex(concat!(970            "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",971            "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",972            "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",973            "3ff4def08e4b7a9de576d26586cec64b6116",974            "1ae10b594f09e26a7e902ecbd0600691",975        ));976        let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();977        assert_eq!(978            cipher979                .encrypt(980                    Nonce::from_slice(&nonce),981                    Payload {982                        msg: &plaintext,983                        aad: &aad,984                    },985                )986                .unwrap(),987            expected,988        );989    }990991    #[test]992    fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {993        let key: Vec<u8> = (0_u8..32).collect();994        let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();995        Mac::update(&mut mac, &[]);996        assert_eq!(997            Mac::finalize(mac).into_bytes().as_slice(),998            &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),999        );1000    }1001}