diff --git a/src/pair_code.rs b/src/pair_code.rs index 1aa0c4844..afeb2749a 100644 --- a/src/pair_code.rs +++ b/src/pair_code.rs @@ -209,7 +209,7 @@ impl Client { pairing_ref, phone_jid: phone_number, pair_code: code.clone(), - ephemeral_keypair, + ephemeral_keypair: Box::new(ephemeral_keypair), }; // Dispatch event for user to display the code diff --git a/wacore/libsignal/src/core/curve.rs b/wacore/libsignal/src/core/curve.rs index 1846472df..24a4ab7a0 100644 --- a/wacore/libsignal/src/core/curve.rs +++ b/wacore/libsignal/src/core/curve.rs @@ -203,12 +203,16 @@ impl fmt::Debug for PublicKey { } use curve25519_dalek::edwards::CompressedEdwardsY; +use curve25519_dalek::scalar::Scalar; use std::sync::OnceLock; -/// Cached Edwards public key data for XEdDSA signing. -/// This avoids an expensive scalar multiplication on every signature. +/// Cached data for XEdDSA signing. +/// This avoids an expensive scalar multiplication on every signature +/// and caches the scalar representation to avoid repeated modular reduction. #[derive(Debug, Clone, Copy, Eq, PartialEq)] struct EdwardsCacheData { + /// Cached scalar representation of the private key + scalar: Scalar, ed_public_key: CompressedEdwardsY, sign_bit: u8, } @@ -251,7 +255,7 @@ impl From for PrivateKey { } impl PrivateKey { - /// Lazily computes and caches the Edwards public key data. + /// Lazily computes and caches the signing data (scalar + Edwards public key). #[inline] fn get_edwards_cache(&self) -> &EdwardsCacheData { match &self.key { @@ -259,6 +263,7 @@ impl PrivateKey { edwards_cache.get_or_init(|| { let temp = curve25519::PrivateKey::from(*key); EdwardsCacheData { + scalar: temp.cached_scalar(), ed_public_key: temp.cached_ed_public_key(), sign_bit: temp.cached_sign_bit(), } @@ -324,11 +329,10 @@ impl PrivateKey { ) -> Result<[u8; 64], CurveError> { match &self.key { PrivateKeyData::DjbPrivateKey { key, .. } => { - // Get or compute the Edwards cache (lazy initialization) let cache = self.get_edwards_cache(); - // Reconstruct with cached values (no scalar mult after first call) let private_key = curve25519::PrivateKey::from_bytes_with_cache( *key, + cache.scalar, cache.ed_public_key, cache.sign_bit, ); diff --git a/wacore/libsignal/src/core/curve/curve25519.rs b/wacore/libsignal/src/core/curve/curve25519.rs index 4218ef487..04847b648 100644 --- a/wacore/libsignal/src/core/curve/curve25519.rs +++ b/wacore/libsignal/src/core/curve/curve25519.rs @@ -18,6 +18,12 @@ pub const PRIVATE_KEY_LENGTH: usize = 32; pub const PUBLIC_KEY_LENGTH: usize = 32; pub const SIGNATURE_LENGTH: usize = 64; +/// Sentinel value for `sign_bit` indicating the Edwards cache was not initialized. +/// Valid sign_bit values are only 0x00 or 0x80 (the MSB of the compressed Edwards Y coordinate). +/// Using 0xFF as an invalid sentinel allows `calculate_signature` to detect and panic +/// instead of silently producing invalid signatures. +const SIGN_BIT_NOT_INITIALIZED: u8 = 0xFF; + /// XEdDSA hash prefix as per the specification. /// This is 0xFE followed by 31 bytes of 0xFF. /// See: https://signal.org/docs/specifications/xeddsa/#xeddsa @@ -29,6 +35,9 @@ static XEDDSA_HASH_PREFIX: [u8; 32] = [ #[derive(Clone)] pub struct PrivateKey { secret: StaticSecret, + /// Cached scalar representation of the private key for signing. + /// Avoids `from_bytes_mod_order` on every signature. + scalar: Scalar, /// Cached Edwards public key (compressed form) derived from the X25519 key. /// Caching this avoids an expensive scalar multiplication on every signature. /// See: https://signal.org/docs/specifications/xeddsa/#curve25519 @@ -38,15 +47,15 @@ pub struct PrivateKey { } impl PrivateKey { - /// Computes the cached Edwards public key and sign bit from a StaticSecret. + /// Computes the cached scalar, Edwards public key, and sign bit from a StaticSecret. #[inline] - fn compute_ed_public_key(secret: &StaticSecret) -> (CompressedEdwardsY, u8) { + fn compute_edwards_cache(secret: &StaticSecret) -> (Scalar, CompressedEdwardsY, u8) { let key_data = secret.to_bytes(); - let a = Scalar::from_bytes_mod_order(key_data); - let ed_public_key_point = &a * ED25519_BASEPOINT_TABLE; + let scalar = Scalar::from_bytes_mod_order(key_data); + let ed_public_key_point = &scalar * ED25519_BASEPOINT_TABLE; let ed_public_key = ed_public_key_point.compress(); let sign_bit = ed_public_key.as_bytes()[31] & 0b1000_0000_u8; - (ed_public_key, sign_bit) + (scalar, ed_public_key, sign_bit) } /// Generates a new random private key with eagerly-computed Edwards cache. @@ -63,9 +72,10 @@ impl PrivateKey { bytes = scalar::clamp_integer(bytes); let secret = StaticSecret::from(bytes); - let (ed_public_key, sign_bit) = Self::compute_ed_public_key(&secret); + let (scalar, ed_public_key, sign_bit) = Self::compute_edwards_cache(&secret); PrivateKey { secret, + scalar, ed_public_key, sign_bit, } @@ -73,17 +83,15 @@ impl PrivateKey { /// Generates a new random private key WITHOUT computing the Edwards cache. /// - /// # Safety Contract - /// This function is for internal use when the key will be wrapped in a higher-level - /// type with lazy initialization (e.g., `curve::PrivateKey` with `OnceLock`). + /// This skips the expensive scalar multiplication required for XEdDSA signing. + /// Use this when the key will be wrapped in a higher-level type with lazy + /// initialization (e.g., `curve::PrivateKey` with `OnceLock`). /// - /// **WARNING**: Do NOT call `calculate_signature` on a `PrivateKey` created with this - /// function - it will produce INVALID signatures. The `ed_public_key` and `sign_bit` - /// fields contain dummy values (all zeros) that are not valid for signing. + /// # Panics /// - /// Safe operations: `private_key_bytes()`, `derive_public_key_bytes()`, `calculate_agreement()` + /// Calling `calculate_signature` on a key created with this function will panic. #[inline] - pub fn new_without_cache(csprng: &mut R) -> Self + pub(super) fn new_without_cache(csprng: &mut R) -> Self where R: CryptoRng + Rng, { @@ -92,25 +100,29 @@ impl PrivateKey { bytes = scalar::clamp_integer(bytes); let secret = StaticSecret::from(bytes); - // Dummy values - signing with these will produce INVALID signatures + // Sentinel values - calculate_signature will panic if called PrivateKey { secret, + scalar: Scalar::ZERO, ed_public_key: CompressedEdwardsY::default(), - sign_bit: 0, + sign_bit: SIGN_BIT_NOT_INITIALIZED, } } - /// Creates a PrivateKey from raw bytes with pre-computed cached values. - /// This avoids the expensive scalar multiplication when the cached values are already known. + /// Creates a PrivateKey from raw bytes with ALL pre-computed cached values. + /// This is the most efficient constructor - avoids scalar multiplication AND + /// scalar modular reduction when all cached values are already available. #[inline] pub fn from_bytes_with_cache( private_key: [u8; PRIVATE_KEY_LENGTH], + scalar: Scalar, ed_public_key: CompressedEdwardsY, sign_bit: u8, ) -> Self { let secret = StaticSecret::from(scalar::clamp_integer(private_key)); PrivateKey { secret, + scalar, ed_public_key, // Mask to ensure only valid sign bit values (0x00 or 0x80) sign_bit: sign_bit & 0b1000_0000_u8, @@ -129,16 +141,29 @@ impl PrivateKey { self.sign_bit } + /// Returns the cached scalar representation. + #[inline] + pub fn cached_scalar(&self) -> Scalar { + self.scalar + } + /// Creates a PrivateKey from raw bytes WITHOUT computing the Edwards cache. - /// Use this for operations that don't need signatures (e.g., key agreement, public key derivation). + /// + /// Use this for operations that don't need signatures (key agreement, public key + /// derivation). Use `from_bytes_with_cache` if you need signing. + /// + /// # Panics + /// + /// Calling `calculate_signature` on a key created with this function will panic. #[inline] - pub fn from_bytes_without_cache(private_key: [u8; PRIVATE_KEY_LENGTH]) -> Self { + pub(super) fn from_bytes_without_cache(private_key: [u8; PRIVATE_KEY_LENGTH]) -> Self { let secret = StaticSecret::from(scalar::clamp_integer(private_key)); - // Use dummy values - these should never be accessed for non-signature operations + // Sentinel values - calculate_signature will panic if called PrivateKey { secret, + scalar: Scalar::ZERO, ed_public_key: CompressedEdwardsY::default(), - sign_bit: 0, + sign_bit: SIGN_BIT_NOT_INITIALIZED, } } @@ -163,6 +188,12 @@ impl PrivateKey { /// /// Performance: This implementation caches the Edwards public key point to avoid /// the expensive scalar multiplication on every signature (roughly 2x speedup). + /// + /// # Panics + /// + /// Panics if the key was created with `new_without_cache` or `from_bytes_without_cache`. + /// These constructors skip Edwards cache computation for performance; use + /// `from_bytes_with_cache` or the higher-level `curve::PrivateKey` API instead. pub fn calculate_signature( &self, csprng: &mut R, @@ -171,15 +202,19 @@ impl PrivateKey { where R: CryptoRng + Rng, { + assert!( + self.sign_bit != SIGN_BIT_NOT_INITIALIZED, + "cannot sign with a PrivateKey created via new_without_cache or from_bytes_without_cache; \ + use from_bytes_with_cache or the higher-level curve::PrivateKey API" + ); + let mut random_bytes = [0u8; 64]; csprng.fill_bytes(&mut random_bytes); let key_data = self.secret.to_bytes(); - let a = Scalar::from_bytes_mod_order(key_data); - // Use cached Edwards public key instead of recomputing: &a * ED25519_BASEPOINT_TABLE + // hash1 = SHA512(prefix || privKey || message || random) let mut hash1 = Sha512::new(); - // Use static hash prefix instead of allocating on every call hash1.update(&XEDDSA_HASH_PREFIX[..]); hash1.update(&key_data[..]); for message_piece in message { @@ -190,6 +225,7 @@ impl PrivateKey { let r = Scalar::from_hash(hash1); let cap_r = (&r * ED25519_BASEPOINT_TABLE).compress(); + // hash = SHA512(R || edPubKey || message) let mut hash = Sha512::new(); hash.update(cap_r.as_bytes()); hash.update(self.ed_public_key.as_bytes()); @@ -198,7 +234,7 @@ impl PrivateKey { } let h = Scalar::from_hash(hash); - let s = (h * a) + r; + let s = (h * self.scalar) + r; let mut result = [0u8; SIGNATURE_LENGTH]; result[..32].copy_from_slice(cap_r.as_bytes()); @@ -261,9 +297,10 @@ impl PrivateKey { impl From<[u8; PRIVATE_KEY_LENGTH]> for PrivateKey { fn from(private_key: [u8; 32]) -> Self { let secret = StaticSecret::from(scalar::clamp_integer(private_key)); - let (ed_public_key, sign_bit) = Self::compute_ed_public_key(&secret); + let (scalar, ed_public_key, sign_bit) = Self::compute_edwards_cache(&secret); PrivateKey { secret, + scalar, ed_public_key, sign_bit, } diff --git a/wacore/src/pair_code.rs b/wacore/src/pair_code.rs index 74a8a0676..6d59662ee 100644 --- a/wacore/src/pair_code.rs +++ b/wacore/src/pair_code.rs @@ -122,7 +122,7 @@ pub enum PairCodeState { /// The 8-character pair code (needed to decrypt primary's ephemeral key). pair_code: String, /// Ephemeral keypair generated for this session. - ephemeral_keypair: KeyPair, + ephemeral_keypair: Box, }, /// Pairing completed (success or failure). Completed,