lib.rs

35.1 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    let Ok(key) = <[u8; 32]>::try_from(public_key) else {430        return false;431    };432    X25519_SMALL_ORDER_PUBKEYS433        .iter()434        .any(|low| low.ct_eq(&key).unwrap_u8() == 1)435}436437impl Dh for RejectZeroDh {438    fn name(&self) -> &'static str {439        self.inner.name()440    }441442    fn pub_len(&self) -> usize {443        self.inner.pub_len()444    }445446    fn priv_len(&self) -> usize {447        self.inner.priv_len()448    }449450    fn set(&mut self, private_key: &[u8]) {451        self.inner.set(private_key);452    }453454    fn generate(&mut self, rng: &mut dyn Random) -> Result<(), Error> {455        self.inner.generate(rng)456    }457458    fn pubkey(&self) -> &[u8] {459        self.inner.pubkey()460    }461462    fn privkey(&self) -> &[u8] {463        self.inner.privkey()464    }465466    fn dh(&self, public_key: &[u8], output: &mut [u8]) -> Result<(), Error> {467        if is_small_order_x25519_pubkey(public_key) {468            return Err(Error::Dh);469        }470        self.inner.dh(public_key, output)?;471        let used = self.inner.dh_len();472        if output[..used].ct_eq(&vec![0_u8; used]).unwrap_u8() == 1 {473            output[..used].zeroize();474            return Err(Error::Dh);475        }476        Ok(())477    }478479    fn dh_len(&self) -> usize {480        self.inner.dh_len()481    }482}483484#[cfg(target_arch = "wasm32")]485mod wasm {486    use super::CoreNoiseSession;487    use wasm_bindgen::prelude::*;488489    fn js_error(message: String) -> JsError {490        JsError::new(&message)491    }492493    #[wasm_bindgen]494    pub struct WasmNoiseSession {495        inner: CoreNoiseSession,496    }497498    #[wasm_bindgen]499    impl WasmNoiseSession {500        #[wasm_bindgen(js_name = nkInitiator)]501        pub fn nk_initiator(remote_public_key: &[u8]) -> Result<WasmNoiseSession, JsError> {502            Ok(Self {503                inner: CoreNoiseSession::nk_initiator(remote_public_key).map_err(js_error)?,504            })505        }506507        #[wasm_bindgen(js_name = nkResponder)]508        pub fn nk_responder(509            local_private_key: &[u8],510            local_public_key: &[u8],511        ) -> Result<WasmNoiseSession, JsError> {512            Ok(Self {513                inner: CoreNoiseSession::nk_responder(local_private_key, local_public_key)514                    .map_err(js_error)?,515            })516        }517518        #[wasm_bindgen(js_name = nkInitiatorWithEphemeralForTest)]519        pub fn nk_initiator_with_ephemeral_for_test(520            remote_public_key: &[u8],521            ephemeral_private_key: &[u8],522        ) -> Result<WasmNoiseSession, JsError> {523            Ok(Self {524                inner: CoreNoiseSession::nk_initiator_with_ephemeral_for_test(525                    remote_public_key,526                    ephemeral_private_key,527                )528                .map_err(js_error)?,529            })530        }531532        #[wasm_bindgen(js_name = nkResponderWithEphemeralForTest)]533        pub fn nk_responder_with_ephemeral_for_test(534            local_private_key: &[u8],535            local_public_key: &[u8],536            ephemeral_private_key: &[u8],537        ) -> Result<WasmNoiseSession, JsError> {538            Ok(Self {539                inner: CoreNoiseSession::nk_responder_with_ephemeral_for_test(540                    local_private_key,541                    local_public_key,542                    ephemeral_private_key,543                )544                .map_err(js_error)?,545            })546        }547548        #[wasm_bindgen(js_name = kkInitiator)]549        pub fn kk_initiator(550            local_private_key: &[u8],551            local_public_key: &[u8],552            remote_public_key: &[u8],553        ) -> Result<WasmNoiseSession, JsError> {554            Ok(Self {555                inner: CoreNoiseSession::kk_initiator(556                    local_private_key,557                    local_public_key,558                    remote_public_key,559                )560                .map_err(js_error)?,561            })562        }563564        #[wasm_bindgen(js_name = kkResponder)]565        pub fn kk_responder(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_responder(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 = kkInitiatorWithEphemeralForTest)]581        pub fn kk_initiator_with_ephemeral_for_test(582            local_private_key: &[u8],583            local_public_key: &[u8],584            remote_public_key: &[u8],585            ephemeral_private_key: &[u8],586        ) -> Result<WasmNoiseSession, JsError> {587            Ok(Self {588                inner: CoreNoiseSession::kk_initiator_with_ephemeral_for_test(589                    local_private_key,590                    local_public_key,591                    remote_public_key,592                    ephemeral_private_key,593                )594                .map_err(js_error)?,595            })596        }597598        #[wasm_bindgen(js_name = kkResponderWithEphemeralForTest)]599        pub fn kk_responder_with_ephemeral_for_test(600            local_private_key: &[u8],601            local_public_key: &[u8],602            remote_public_key: &[u8],603            ephemeral_private_key: &[u8],604        ) -> Result<WasmNoiseSession, JsError> {605            Ok(Self {606                inner: CoreNoiseSession::kk_responder_with_ephemeral_for_test(607                    local_private_key,608                    local_public_key,609                    remote_public_key,610                    ephemeral_private_key,611                )612                .map_err(js_error)?,613            })614        }615616        #[wasm_bindgen(js_name = writeHandshake)]617        pub fn write_handshake(&mut self, payload: &[u8]) -> Result<Vec<u8>, JsError> {618            self.inner.write_handshake(payload).map_err(js_error)619        }620621        #[wasm_bindgen(js_name = readHandshake)]622        pub fn read_handshake(&mut self, message: &[u8]) -> Result<Vec<u8>, JsError> {623            self.inner.read_handshake(message).map_err(js_error)624        }625626        pub fn encrypt(&mut self, plaintext: &[u8]) -> Result<Vec<u8>, JsError> {627            self.inner.encrypt(plaintext).map_err(js_error)628        }629630        pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result<Vec<u8>, JsError> {631            self.inner.decrypt(ciphertext).map_err(js_error)632        }633634        #[wasm_bindgen(getter, js_name = handshakeHash)]635        pub fn handshake_hash(&self) -> Result<Vec<u8>, JsError> {636            self.inner.handshake_hash().map_err(js_error)637        }638639        #[wasm_bindgen(js_name = rekeyOutgoing)]640        pub fn rekey_outgoing(&mut self) -> Result<(), JsError> {641            self.inner.rekey_outgoing().map_err(js_error)642        }643644        #[wasm_bindgen(js_name = rekeyIncoming)]645        pub fn rekey_incoming(&mut self) -> Result<(), JsError> {646            self.inner.rekey_incoming().map_err(js_error)647        }648    }649}650651#[cfg(not(target_arch = "wasm32"))]652mod jni_bridge;653654#[cfg(test)]655mod tests {656    use super::{657        CoreNoiseSession, KK, NK, X25519_SMALL_ORDER_PUBKEYS, core_builder,658        is_small_order_x25519_pubkey,659    };660    use blake2::{661        Blake2sMac256,662        digest::{KeyInit as BlakeKeyInit, Mac},663    };664    use chacha20poly1305::{665        ChaCha20Poly1305, Nonce,666        aead::{Aead, Payload},667    };668    use snow::params::NoiseParams;669    use x25519_dalek::{PublicKey, StaticSecret};670671    fn keypair(seed: u8) -> ([u8; 32], [u8; 32]) {672        let private = [seed; 32];673        let public = *PublicKey::from(&StaticSecret::from(private)).as_bytes();674        (private, public)675    }676677    fn hex(value: &str) -> Vec<u8> {678        assert!(value.len().is_multiple_of(2));679        value680            .as_bytes()681            .chunks_exact(2)682            .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())683            .collect()684    }685686    fn official_pair(687        pattern: &str,688        initiator_static: Option<&str>,689        initiator_ephemeral: &str,690        initiator_remote_static: &str,691        responder_static: &str,692        responder_ephemeral: &str,693        responder_remote_static: Option<&str>,694    ) -> (CoreNoiseSession, CoreNoiseSession) {695        let prologue = hex("4a6f686e2047616c74");696        let responder_private = hex(responder_static);697        let responder_public = PublicKey::from(&StaticSecret::from(698            <[u8; 32]>::try_from(&responder_private[..]).unwrap(),699        ));700        assert_eq!(701            responder_public.as_bytes(),702            &hex(initiator_remote_static)[..]703        );704705        let params: NoiseParams = pattern.parse().unwrap();706        let initiator_ephemeral = hex(initiator_ephemeral);707        let initiator_static = initiator_static.map(hex);708        let responder_ephemeral = hex(responder_ephemeral);709        let responder_remote_static = responder_remote_static.map(hex);710        let mut initiator_builder = core_builder(params.clone())711            .prologue(&prologue)712            .unwrap()713            .remote_public_key(responder_public.as_bytes())714            .unwrap()715            .fixed_ephemeral_key_for_testing_only(&initiator_ephemeral);716        if let Some(private) = &initiator_static {717            initiator_builder = initiator_builder.local_private_key(private).unwrap();718        }719720        let mut responder_builder = core_builder(params)721            .prologue(&prologue)722            .unwrap()723            .local_private_key(&responder_private)724            .unwrap()725            .fixed_ephemeral_key_for_testing_only(&responder_ephemeral);726        if let Some(remote_public) = &responder_remote_static {727            responder_builder = responder_builder.remote_public_key(remote_public).unwrap();728        }729730        (731            CoreNoiseSession::handshake(initiator_builder.build_initiator().unwrap()),732            CoreNoiseSession::handshake(responder_builder.build_responder().unwrap()),733        )734    }735736    fn assert_cacophony_vector(737        mut initiator: CoreNoiseSession,738        mut responder: CoreNoiseSession,739        handshake_hash: &str,740        messages: &[(&str, &str)],741    ) {742        for (index, (payload, expected_ciphertext)) in messages.iter().enumerate() {743            let plaintext = hex(payload);744            let expected = hex(expected_ciphertext);745            if index < 2 {746                let (sender, receiver) = if index % 2 == 0 {747                    (&mut initiator, &mut responder)748                } else {749                    (&mut responder, &mut initiator)750                };751                let actual = sender.write_handshake(&plaintext).unwrap();752                assert_eq!(actual, expected, "handshake message {index}");753                assert_eq!(754                    receiver.read_handshake(&actual).unwrap(),755                    plaintext,756                    "handshake payload {index}",757                );758            } else {759                let (sender, receiver) = if index % 2 == 0 {760                    (&mut initiator, &mut responder)761                } else {762                    (&mut responder, &mut initiator)763                };764                let actual = sender.encrypt(&plaintext).unwrap();765                assert_eq!(actual, expected, "transport message {index}");766                assert_eq!(767                    receiver.decrypt(&actual).unwrap(),768                    plaintext,769                    "transport payload {index}",770                );771            }772        }773        let expected_hash = hex(handshake_hash);774        assert_eq!(initiator.handshake_hash().unwrap(), expected_hash);775        assert_eq!(responder.handshake_hash().unwrap(), expected_hash);776    }777778    fn complete(initiator: &mut CoreNoiseSession, responder: &mut CoreNoiseSession) {779        let message1 = initiator.write_handshake(&[]).unwrap();780        assert!(responder.read_handshake(&message1).unwrap().is_empty());781        let message2 = responder.write_handshake(&[]).unwrap();782        assert!(initiator.read_handshake(&message2).unwrap().is_empty());783        assert_eq!(784            initiator.handshake_hash().unwrap(),785            responder.handshake_hash().unwrap()786        );787    }788789    #[test]790    fn nk_round_trip_uses_one_state_machine() {791        let (responder_private, responder_public) = keypair(7);792        let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();793        let mut responder =794            CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();795        complete(&mut initiator, &mut responder);796797        let ciphertext = initiator.encrypt(b"peach").unwrap();798        assert_eq!(responder.decrypt(&ciphertext).unwrap(), b"peach");799        let response = responder.encrypt(b"sync").unwrap();800        assert_eq!(initiator.decrypt(&response).unwrap(), b"sync");801    }802803    // PB-26-014: contributory behaviour — a small-order remote static must804    // fail the handshake at the DH step instead of contributing a constant805    // low-entropy secret to the keying material.806    #[test]807    fn small_order_remote_static_fails_handshake() {808        let (local_private, local_public) = keypair(11);809        let mut initiator = CoreNoiseSession::kk_initiator(810            &local_private,811            &local_public,812            &X25519_SMALL_ORDER_PUBKEYS[3],813        )814        .unwrap();815        let result = initiator.write_handshake(&[]);816        assert!(result.is_err());817        // Every small-order key is recognized by the guard; real keys never are.818        for low in X25519_SMALL_ORDER_PUBKEYS.iter() {819            assert!(is_small_order_x25519_pubkey(low));820        }821        let (_, real_public) = keypair(9);822        assert!(!is_small_order_x25519_pubkey(&real_public));823    }824825    #[test]826    fn kk_round_trip_authenticates_both_static_keys() {827        let (initiator_private, initiator_public) = keypair(11);828        let (responder_private, responder_public) = keypair(19);829        let mut initiator = CoreNoiseSession::kk_initiator(830            &initiator_private,831            &initiator_public,832            &responder_public,833        )834        .unwrap();835        let mut responder = CoreNoiseSession::kk_responder(836            &responder_private,837            &responder_public,838            &initiator_public,839        )840        .unwrap();841        complete(&mut initiator, &mut responder);842    }843844    #[test]845    fn mismatched_local_keypair_is_rejected() {846        let (private, _) = keypair(2);847        let (_, wrong_public) = keypair(3);848        let result = CoreNoiseSession::nk_responder(&private, &wrong_public);849        assert!(matches!(result, Err(error) if error.contains("does not match")));850    }851852    #[test]853    fn tampered_transport_frame_is_rejected() {854        let (responder_private, responder_public) = keypair(23);855        let mut initiator = CoreNoiseSession::nk_initiator(&responder_public).unwrap();856        let mut responder =857            CoreNoiseSession::nk_responder(&responder_private, &responder_public).unwrap();858        complete(&mut initiator, &mut responder);859        let mut ciphertext = initiator.encrypt(b"vault").unwrap();860        ciphertext[0] ^= 0x80;861        assert!(responder.decrypt(&ciphertext).is_err());862    }863864    #[test]865    fn cacophony_nk_vector_matches_shared_core() {866        let pair = official_pair(867            NK,868            None,869            "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",870            "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",871            "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",872            "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",873            None,874        );875        assert_cacophony_vector(876            pair.0,877            pair.1,878            "d7244d974066aae2376f7ba5534f60a6e4e82cd7c9751e226cae3928e6b49f14",879            &[880                (881                    "4c756477696720766f6e204d69736573",882                    "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c794454ae7612d1724af42adb130160a9a94e67b5b169b4e00c189f6467cd17eb7cad",883                ),884                (885                    "4d757272617920526f746862617264",886                    "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843986a5c929337e337ac8b4a074af12ab9f76318a5f18c8b599a443af07383ce",887                ),888                (889                    "462e20412e20486179656b",890                    "550027c7a5d450017bcb5e12b8253b1c53fd2213aeda84891d5f95",891                ),892                (893                    "4361726c204d656e676572",894                    "dfbce0c38210ccee35e830aca9dd8b8b3997b933e75bfc8864b759",895                ),896            ],897        );898    }899900    #[test]901    fn cacophony_kk_vector_matches_shared_core() {902        let pair = official_pair(903            KK,904            Some("e61ef9919cde45dd5f82166404bd08e38bceb5dfdfded0a34c8df7ed542214d1"),905            "893e28b9dc6ca8d611ab664754b8ceb7bac5117349a4439a6b0569da977c464a",906            "31e0303fd6418d2f8c0e78b91f22e8caed0fbe48656dcf4767e4834f701b8f62",907            "4a3acbfdb163dec651dfa3194dece676d437029c62a408b4c5ea9114246e4893",908            "bbdb4cdbd309f1a1f2e1456967fe288cadd6f712d65dc7b7793d5e63da6b375b",909            Some("6bc3822a2aa7f4e6981d6538692b3cdf3e6df9eea6ed269eb41d93c22757b75a"),910        );911        assert_cacophony_vector(912            pair.0,913            pair.1,914            "1362b8627a00907ce11e558aba8ce7cbca88e83f0e84ce7db5159b1c3e25ab59",915            &[916                (917                    "4c756477696720766f6e204d69736573",918                    "ca35def5ae56cec33dc2036731ab14896bc4c75dbb07a61f879f8e3afa4c7944266a5f53784aa3becb0f7485c2759c328937867a4cbaafef07422b0725e098be",919                ),920                (921                    "4d757272617920526f746862617264",922                    "95ebc60d2b1fa672c1f46a8aa265ef51bfe38e7ccb39ec5be34069f144808843008aeea5d76d6abcbab87a18502c8a8352d9933ac11e2a7d228038d721e31e",923                ),924                (925                    "462e20412e20486179656b",926                    "5f92113edf78c3e56e6d67201f5f9e0c8f2930c3e1ffb64ede0358",927                ),928                (929                    "4361726c204d656e676572",930                    "30ebbd9cdcef7f40d99c8cd11e880dac28f5c9e5032c1059b3b56a",931                ),932            ],933        );934    }935936    #[test]937    fn rfc_8439_chacha20_poly1305_aead_vector_matches_snow_cipher() {938        let key = hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");939        let nonce = hex("070000004041424344454647");940        let aad = hex("50515253c0c1c2c3c4c5c6c7");941        let plaintext = hex(concat!(942            "4c616469657320616e642047656e746c656d656e206f662074686520636c6173",943            "73206f66202739393a204966204920636f756c64206f6666657220796f75206f",944            "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73",945            "637265656e20776f756c642062652069742e",946        ));947        let expected = hex(concat!(948            "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6",949            "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36",950            "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc",951            "3ff4def08e4b7a9de576d26586cec64b6116",952            "1ae10b594f09e26a7e902ecbd0600691",953        ));954        let cipher = ChaCha20Poly1305::new_from_slice(&key).unwrap();955        assert_eq!(956            cipher957                .encrypt(958                    Nonce::from_slice(&nonce),959                    Payload {960                        msg: &plaintext,961                        aad: &aad,962                    },963                )964                .unwrap(),965            expected,966        );967    }968969    #[test]970    fn blake2_reference_keyed_vector_matches_snow_hash_dependency() {971        let key: Vec<u8> = (0_u8..32).collect();972        let mut mac = <Blake2sMac256 as BlakeKeyInit>::new_from_slice(&key).unwrap();973        Mac::update(&mut mac, &[]);974        assert_eq!(975            Mac::finalize(mac).into_bytes().as_slice(),976            &hex("48a8997da407876b3d79c0d92325ad3b89cbb754d86ab71aee047ad345fd2c49"),977        );978    }979}