Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pair_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions wacore/libsignal/src/core/curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -251,14 +255,15 @@ impl From<PrivateKeyData> 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 {
PrivateKeyData::DjbPrivateKey { key, edwards_cache } => {
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(),
}
Expand Down Expand Up @@ -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,
);
Expand Down
91 changes: 64 additions & 27 deletions wacore/libsignal/src/core/curve/curve25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -63,27 +72,26 @@ 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,
}
}

/// 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<R>(csprng: &mut R) -> Self
pub(super) fn new_without_cache<R>(csprng: &mut R) -> Self
where
R: CryptoRng + Rng,
{
Expand All @@ -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,
Expand All @@ -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,
}
}

Expand All @@ -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<R>(
&self,
csprng: &mut R,
Expand All @@ -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 {
Expand All @@ -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());
Expand All @@ -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());
Expand Down Expand Up @@ -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,
}
Expand Down
2 changes: 1 addition & 1 deletion wacore/src/pair_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyPair>,
},
/// Pairing completed (success or failure).
Completed,
Expand Down