diff --git a/wacore/libsignal/src/core/curve.rs b/wacore/libsignal/src/core/curve.rs index c6c37734b..c2e8ee1c3 100644 --- a/wacore/libsignal/src/core/curve.rs +++ b/wacore/libsignal/src/core/curve.rs @@ -303,6 +303,21 @@ impl PrivateKey { } } + /// Pre-derives the XEdDSA signing cache (scalar + Edwards point). Clones + /// of this key carry the warm cache, so warming once at rest lets every + /// later signature skip the basepoint multiplication. + pub fn precompute_signing_cache(&self) { + let _ = self.get_edwards_cache(); + } + + /// Test-only visibility into the lazy cache, to pin the warm-clone contract. + #[cfg(test)] + pub(crate) fn has_warm_signing_cache(&self) -> bool { + match &self.key { + PrivateKeyData::DjbPrivateKey { edwards_cache, .. } => edwards_cache.get().is_some(), + } + } + pub fn deserialize(value: &[u8]) -> Result { if value.len() != curve25519::PRIVATE_KEY_LENGTH { Err(CurveError::BadKeyLength(KeyType::Djb, value.len())) diff --git a/wacore/libsignal/src/protocol/sender_keys.rs b/wacore/libsignal/src/protocol/sender_keys.rs index 001f08876..5239906aa 100644 --- a/wacore/libsignal/src/protocol/sender_keys.rs +++ b/wacore/libsignal/src/protocol/sender_keys.rs @@ -166,9 +166,34 @@ impl SenderChainKey { } } -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct SenderKeyState { state: SenderKeyStateStructure, + /// Parsed signing key with its XEdDSA cache pre-derived, memoized so the + /// per-send signature skips a basepoint multiplication (~18% of a warm + /// group send when re-derived from bytes every message). Clones carry the + /// warm value, and the record cache stores this object back after every + /// send, so the memo persists for the cache lifetime. Never persisted; + /// rebuilt lazily after a cold load. If a signing-key setter is ever + /// added, it must reset this memo. + signing_key_memo: std::sync::OnceLock, +} + +// Manual impl with the signing key REDACTED: the protobuf state embeds the +// serialized private signing key, and the previous derive printed it raw +// into any `{:?}` log or panic message. +impl std::fmt::Debug for SenderKeyState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SenderKeyState") + .field("chain_id", &self.chain_id()) + .field( + "chain_iteration", + &self.sender_chain_key().map(|c| c.iteration()), + ) + .field("message_keys", &self.state.sender_message_keys.len()) + .field("signing_key", &"") + .finish_non_exhaustive() + } } impl SenderKeyState { @@ -188,16 +213,28 @@ impl SenderKeyState { sender_signing_key: Some(sender_key_state_structure::SenderSigningKey { public: Some(Bytes::copy_from_slice(&signature_key.serialize())), private: signature_private_key + .as_ref() .map(|k| Bytes::copy_from_slice(k.serialize().as_ref())), }), sender_message_keys: vec![], }; - Self { state } + let signing_key_memo = std::sync::OnceLock::new(); + if let Some(key) = signature_private_key { + key.precompute_signing_cache(); + let _ = signing_key_memo.set(key); + } + Self { + state, + signing_key_memo, + } } pub(crate) fn from_protobuf(state: SenderKeyStateStructure) -> Self { - Self { state } + Self { + state, + signing_key_memo: std::sync::OnceLock::new(), + } } pub fn message_version(&self) -> u32 { @@ -240,18 +277,33 @@ impl SenderKeyState { } pub fn signing_key_private(&self) -> Result { + if let Some(key) = self.signing_key_memo.get() { + return Ok(key.clone()); + } if let Some(ref signing_key) = self.state.sender_signing_key { let private = signing_key .private .as_ref() .ok_or(InvalidSenderKeySessionError("missing private key bytes"))?; - PrivateKey::deserialize(private) - .map_err(|_| InvalidSenderKeySessionError("invalid private signing key")) + let key = PrivateKey::deserialize(private) + .map_err(|_| InvalidSenderKeySessionError("invalid private signing key"))?; + // Warm BEFORE memoizing: the caller gets a clone, and clones of a + // cold key would each re-derive the cache; clones of a warm one + // carry it. Benign race: concurrent firsts compute equal values. + key.precompute_signing_cache(); + let _ = self.signing_key_memo.set(key.clone()); + Ok(key) } else { Err(InvalidSenderKeySessionError("missing signing key")) } } + /// Test-only: whether the signing-key memo is populated. + #[cfg(test)] + pub(crate) fn signing_key_memo_initialized(&self) -> bool { + self.signing_key_memo.get().is_some() + } + pub(crate) fn as_protobuf(&self) -> SenderKeyStateStructure { self.state.clone() } @@ -545,6 +597,55 @@ mod tests { assert!(state.signing_key_private().is_ok()); } + #[test] + fn signing_key_memo_warms_on_first_use_and_survives_clone() { + let mut rng = rand::make_rng::(); + let signing = crate::core::curve::KeyPair::generate(&mut rng); + let chain_key = [7u8; 32]; + let state = SenderKeyState::new( + 3, + 1, + 0, + &chain_key, + signing.public_key, + Some(signing.private_key), + ); + + // new() received the parsed key: memo pre-populated and pre-warmed. + assert!(state.signing_key_memo_initialized()); + assert!( + state + .signing_key_private() + .expect("memo key") + .has_warm_signing_cache() + ); + + // A cold load (protobuf roundtrip) drops the memo; the first + // signing_key_private() call rebuilds AND warms it, and the clone + // handed back carries the warm cache. + let reloaded = SenderKeyState::from_protobuf(state.as_protobuf()); + assert!(!reloaded.signing_key_memo_initialized()); + let key = reloaded.signing_key_private().expect("reloaded key"); + assert!(key.has_warm_signing_cache()); + assert!(reloaded.signing_key_memo_initialized()); + + // Clones of the state (the per-send record clone) carry the memo. + let cloned = reloaded.clone(); + assert!(cloned.signing_key_memo_initialized()); + assert!( + cloned + .signing_key_private() + .expect("cloned key") + .has_warm_signing_cache() + ); + + // The memoized key still signs correctly. + let msg = b"skmsg"; + let sig = key.calculate_signature(msg, &mut rng).expect("sign"); + let public = reloaded.signing_key_public().expect("public key"); + assert!(public.verify_signature(msg, &sig)); + } + /// Test SenderKeyState chain key operations #[test] fn test_sender_key_state_chain_key_update() {