diff --git a/Cargo.lock b/Cargo.lock index 23e7d5b7d..a5cfa4576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2212,9 +2212,9 @@ name = "wacore-libsignal" version = "0.5.0" dependencies = [ "aes", - "aes-gcm", "arrayref", "async-trait", + "bytes", "cbc", "chrono", "ctr", @@ -2244,7 +2244,6 @@ dependencies = [ name = "wacore-noise" version = "0.5.0" dependencies = [ - "aes-gcm", "anyhow", "bytes", "hkdf", diff --git a/wacore/Cargo.toml b/wacore/Cargo.toml index a83489b17..9e2ea8204 100644 --- a/wacore/Cargo.toml +++ b/wacore/Cargo.toml @@ -18,7 +18,6 @@ js = ["getrandom/wasm_js"] [dependencies] aes = { workspace = true } -aes-gcm = { workspace = true } anyhow = { workspace = true } async-channel = { workspace = true } async-lock = { workspace = true } @@ -55,6 +54,7 @@ wacore-noise = { workspace = true } waproto = { workspace = true } [dev-dependencies] +aes-gcm = { workspace = true } futures = { workspace = true, features = ["executor"] } iai-callgrind = { workspace = true } tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/wacore/libsignal/Cargo.toml b/wacore/libsignal/Cargo.toml index 709e4f52b..0b61df0c1 100644 --- a/wacore/libsignal/Cargo.toml +++ b/wacore/libsignal/Cargo.toml @@ -9,9 +9,9 @@ description = "Signal Protocol implementation for the WhatsApp platform" [dependencies] aes = { workspace = true } -aes-gcm = { workspace = true } arrayref = "0.3.9" async-trait = { workspace = true } +bytes = { workspace = true } cbc = { version = "0.2", features = ["alloc"] } chrono = { workspace = true, features = ["now"] } ctr = { workspace = true } diff --git a/wacore/libsignal/src/crypto/aes_cbc.rs b/wacore/libsignal/src/crypto/aes_cbc.rs index 06498aed5..8fcd5391f 100644 --- a/wacore/libsignal/src/crypto/aes_cbc.rs +++ b/wacore/libsignal/src/crypto/aes_cbc.rs @@ -5,9 +5,7 @@ use std::result::Result; -use aes::Aes256; -use aes::cipher::block_padding::Pkcs7; -use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit}; +use crate::crypto::provider::provider; #[derive(Debug, displaydoc::Display, thiserror::Error)] pub enum EncryptionError { @@ -31,35 +29,11 @@ pub fn aes_256_cbc_encrypt_into( iv: &[u8], output: &mut Vec, ) -> Result<(), EncryptionError> { - // Calculate the space needed for encryption + PKCS7 padding - // PKCS7 padding can add 1-16 bytes (always adds at least 1 byte) - let padding_needed = 16 - (ptext.len() % 16); - let encrypted_size = ptext.len() + padding_needed; - - let start_pos = output.len(); - - // Reserve space for the encrypted data - output.resize(start_pos + encrypted_size, 0); - - // Copy plaintext to the buffer - output[start_pos..start_pos + ptext.len()].copy_from_slice(ptext); - - // Create encryptor and encrypt in place - let encryptor = cbc::Encryptor::::new_from_slices(key, iv) - .map_err(|_| EncryptionError::BadKeyOrIv)?; - - // Encrypt the data in place with proper padding - let encrypted_len = { - let encrypted_slice = encryptor - .encrypt_padded::(&mut output[start_pos..], ptext.len()) - .map_err(|_| EncryptionError::BadPadding)?; - encrypted_slice.len() - }; - - // Resize to actual encrypted length - output.truncate(start_pos + encrypted_len); - - Ok(()) + let key: &[u8; 32] = key.try_into().map_err(|_| EncryptionError::BadKeyOrIv)?; + let iv: &[u8; 16] = iv.try_into().map_err(|_| EncryptionError::BadKeyOrIv)?; + provider() + .aes_256_cbc_encrypt(key, iv, ptext, output) + .map_err(|_| EncryptionError::BadPadding) } /// The output buffer is cleared and filled with the decrypted plaintext. @@ -69,27 +43,11 @@ pub fn aes_256_cbc_decrypt_into( iv: &[u8], output: &mut Vec, ) -> Result<(), DecryptionError> { - if ctext.is_empty() || !ctext.len().is_multiple_of(16) { - return Err(DecryptionError::BadCiphertext( - "ciphertext length must be a non-zero multiple of 16", - )); - } - - output.clear(); - output.reserve(ctext.len()); - output.extend_from_slice(ctext); - - let decryptor = cbc::Decryptor::::new_from_slices(key, iv) - .map_err(|_| DecryptionError::BadKeyOrIv)?; - - let decrypted = decryptor - .decrypt_padded::(output) - .map_err(|_| DecryptionError::BadCiphertext("failed to decrypt"))?; - - let decrypted_len = decrypted.len(); - output.truncate(decrypted_len); - - Ok(()) + let key: &[u8; 32] = key.try_into().map_err(|_| DecryptionError::BadKeyOrIv)?; + let iv: &[u8; 16] = iv.try_into().map_err(|_| DecryptionError::BadKeyOrIv)?; + provider() + .aes_256_cbc_decrypt(key, iv, ctext, output) + .map_err(|_| DecryptionError::BadCiphertext("failed to decrypt")) } #[cfg(test)] diff --git a/wacore/libsignal/src/crypto/mod.rs b/wacore/libsignal/src/crypto/mod.rs index ba53fb0cc..8690b4472 100644 --- a/wacore/libsignal/src/crypto/mod.rs +++ b/wacore/libsignal/src/crypto/mod.rs @@ -8,7 +8,8 @@ mod hash; mod aes_cbc; mod aes_ctr; -mod aes_gcm; +pub(crate) mod aes_gcm; +mod provider; pub use aes_cbc::{ DecryptionError, EncryptionError, aes_256_cbc_decrypt_into, aes_256_cbc_encrypt_into, @@ -19,3 +20,71 @@ pub use error::{Error, Result}; pub use hash::{ CryptographicHash, CryptographicMac, SHA1_OUTPUT_SIZE, SHA256_OUTPUT_SIZE, SHA512_OUTPUT_SIZE, }; +pub use provider::{ + CryptoProviderError, GcmInPlaceBuffer, RustCryptoProvider, SignalCryptoProvider, + set_crypto_provider, +}; + +/// AES-256-GCM seal. Appends `ciphertext || tag(16)` to `out`. +/// Delegates to the active [`SignalCryptoProvider`]. +#[inline] +pub fn aes_256_gcm_encrypt( + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8], + out: &mut Vec, +) -> std::result::Result<(), CryptoProviderError> { + provider::provider().aes_256_gcm_encrypt(key, nonce, aad, plaintext, out) +} + +/// AES-256-GCM open. `ciphertext_with_tag` must end with the 16-byte tag. +/// Appends plaintext to `out` on success. +#[inline] +pub fn aes_256_gcm_decrypt( + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + ciphertext_with_tag: &[u8], + out: &mut Vec, +) -> std::result::Result<(), CryptoProviderError> { + provider::provider().aes_256_gcm_decrypt(key, nonce, aad, ciphertext_with_tag, out) +} + +/// HMAC-SHA256 one-shot. Delegates to the active [`SignalCryptoProvider`]. +#[inline] +pub fn hmac_sha256(key: &[u8], input: &[u8]) -> [u8; 32] { + provider::provider().hmac_sha256(key, input) +} + +/// In-place AES-256-GCM seal. On entry `buffer` holds the plaintext; on return +/// it holds `ciphertext || tag` (length grown by 16). Zero allocations with +/// the default [`RustCryptoProvider`]. +#[inline] +pub fn aes_256_gcm_encrypt_in_place( + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut B, +) -> std::result::Result<(), CryptoProviderError> { + provider::provider().aes_256_gcm_encrypt_in_place(key, nonce, aad, buffer) +} + +/// In-place AES-256-GCM open. On entry `buffer` holds `ciphertext || tag`; on +/// success it holds plaintext (length shrunk by 16). +/// +/// On authentication failure ([`CryptoProviderError::AuthFailed`]) the buffer +/// is left in an **indeterminate** state: its length is unchanged but the +/// first `buffer.len() - 16` bytes contain the CTR-XOR output (pseudo- +/// plaintext derived from forged ciphertext) rather than the original +/// ciphertext. Callers **must not** reuse the buffer contents — discard or +/// reinitialize it, and treat the session as compromised. +#[inline] +pub fn aes_256_gcm_decrypt_in_place( + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut B, +) -> std::result::Result<(), CryptoProviderError> { + provider::provider().aes_256_gcm_decrypt_in_place(key, nonce, aad, buffer) +} diff --git a/wacore/libsignal/src/crypto/provider.rs b/wacore/libsignal/src/crypto/provider.rs new file mode 100644 index 000000000..a6ac048fa --- /dev/null +++ b/wacore/libsignal/src/crypto/provider.rs @@ -0,0 +1,422 @@ +//! Pluggable Signal crypto provider. +//! +//! Default uses RustCrypto (soft). Override via [`set_crypto_provider`] to +//! delegate hot-path primitives to a faster backend (e.g. `node:crypto` +//! over a WASM bridge). Must be set before any crypto call. + +use std::sync::OnceLock; + +use aes::Aes256; +use aes::cipher::block_padding::Pkcs7; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyIvInit}; +use bytes::BytesMut; +use hmac::{Hmac, KeyInit, Mac}; +use sha2::Sha256; + +use crate::crypto::aes_gcm::{Aes256GcmDecryption, Aes256GcmEncryption}; + +const GCM_TAG: usize = 16; + +/// Growable byte buffer usable with in-place AES-GCM operations. +/// +/// Implemented for `Vec` and `bytes::BytesMut`. Both already expose +/// mutable-slice access plus `resize`/`truncate`, so providers can do the +/// actual CTR/XOR work without allocating a scratch buffer. +pub trait GcmInPlaceBuffer { + fn as_mut_slice(&mut self) -> &mut [u8]; + fn as_slice(&self) -> &[u8]; + fn len(&self) -> usize { + self.as_slice().len() + } + fn is_empty(&self) -> bool { + self.len() == 0 + } + fn resize(&mut self, new_len: usize, value: u8); + fn truncate(&mut self, len: usize); +} + +impl GcmInPlaceBuffer for Vec { + #[inline] + fn as_mut_slice(&mut self) -> &mut [u8] { + self.as_mut_slice() + } + #[inline] + fn as_slice(&self) -> &[u8] { + self.as_slice() + } + #[inline] + fn resize(&mut self, new_len: usize, value: u8) { + Vec::resize(self, new_len, value); + } + #[inline] + fn truncate(&mut self, len: usize) { + Vec::truncate(self, len); + } +} + +impl GcmInPlaceBuffer for BytesMut { + #[inline] + fn as_mut_slice(&mut self) -> &mut [u8] { + self + } + #[inline] + fn as_slice(&self) -> &[u8] { + self + } + #[inline] + fn resize(&mut self, new_len: usize, value: u8) { + BytesMut::resize(self, new_len, value); + } + #[inline] + fn truncate(&mut self, len: usize) { + BytesMut::truncate(self, len); + } +} + +#[derive(Debug, displaydoc::Display, thiserror::Error)] +pub enum CryptoProviderError { + /// bad key/iv/nonce size or malformed input + BadInput, + /// authentication tag verification failed + AuthFailed, + /// provider backend reported failure + BackendFailed, +} + +/// Pluggable crypto primitives used by libsignal and higher-level callers. +/// +/// All methods are one-shot (no streaming state): inputs fully known at call +/// time, output appended to `out`. Key/nonce sizes are compile-time arrays so +/// implementations can skip size checks. +pub trait SignalCryptoProvider: Send + Sync + 'static { + /// AES-256-CBC encrypt with PKCS7 padding. Ciphertext appended to `out`. + fn aes_256_cbc_encrypt( + &self, + key: &[u8; 32], + iv: &[u8; 16], + plaintext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError>; + + /// AES-256-CBC decrypt with PKCS7 unpadding. `out` is cleared, then filled. + fn aes_256_cbc_decrypt( + &self, + key: &[u8; 32], + iv: &[u8; 16], + ciphertext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError>; + + /// AES-256-GCM seal. Appends `ciphertext || tag(16)` to `out`. + /// Ciphertext length equals plaintext length. + fn aes_256_gcm_encrypt( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError>; + + /// AES-256-GCM open. `ciphertext_with_tag` must end with the 16-byte tag. + /// On success appends plaintext to `out`. On tag failure, `out` is left + /// unchanged and [`CryptoProviderError::AuthFailed`] is returned. + fn aes_256_gcm_decrypt( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + ciphertext_with_tag: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError>; + + /// HMAC-SHA256 one-shot. Zero-alloc output. + fn hmac_sha256(&self, key: &[u8], input: &[u8]) -> [u8; 32]; + + /// In-place AES-256-GCM seal. On entry `buffer` holds the plaintext; on + /// return it holds `ciphertext || tag` (length grown by 16). + /// + /// Default impl delegates to the allocating [`aes_256_gcm_encrypt`] so + /// existing providers keep working; override for zero-allocation variants. + fn aes_256_gcm_encrypt_in_place( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut dyn GcmInPlaceBuffer, + ) -> Result<(), CryptoProviderError> { + let mut out = Vec::with_capacity(buffer.len() + GCM_TAG); + self.aes_256_gcm_encrypt(key, nonce, aad, buffer.as_slice(), &mut out)?; + buffer.resize(out.len(), 0); + buffer.as_mut_slice().copy_from_slice(&out); + Ok(()) + } + + /// In-place AES-256-GCM open. On entry `buffer` holds `ciphertext || tag`; + /// on success it holds plaintext (length shrunk by 16). On auth failure + /// the buffer contents are indeterminate and the caller must treat the + /// session as dead. + /// + /// Default impl delegates to the allocating [`aes_256_gcm_decrypt`]. + fn aes_256_gcm_decrypt_in_place( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut dyn GcmInPlaceBuffer, + ) -> Result<(), CryptoProviderError> { + let mut out = Vec::with_capacity(buffer.len().saturating_sub(GCM_TAG)); + self.aes_256_gcm_decrypt(key, nonce, aad, buffer.as_slice(), &mut out)?; + buffer.resize(out.len(), 0); + buffer.as_mut_slice().copy_from_slice(&out); + Ok(()) + } +} + +static CRYPTO_PROVIDER: OnceLock> = OnceLock::new(); + +/// Install a custom provider. Must be called before any crypto call. Returns +/// `Err` if a provider was already set (including by `get_or_init` of the +/// default fallback). +pub fn set_crypto_provider(provider: impl SignalCryptoProvider) -> Result<(), &'static str> { + CRYPTO_PROVIDER + .set(Box::new(provider)) + .map_err(|_| "crypto provider already set") +} + +#[inline] +pub(crate) fn provider() -> &'static dyn SignalCryptoProvider { + CRYPTO_PROVIDER + .get_or_init(|| Box::new(RustCryptoProvider)) + .as_ref() +} + +/// Pure-Rust fallback implementation. Always used when no custom provider is +/// installed; also used by tests for deterministic behavior. +pub struct RustCryptoProvider; + +impl SignalCryptoProvider for RustCryptoProvider { + fn aes_256_cbc_encrypt( + &self, + key: &[u8; 32], + iv: &[u8; 16], + plaintext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError> { + let padding = 16 - (plaintext.len() % 16); + let encrypted_size = plaintext.len() + padding; + let start = out.len(); + + out.resize(start + encrypted_size, 0); + out[start..start + plaintext.len()].copy_from_slice(plaintext); + + let encryptor = cbc::Encryptor::::new(key.into(), iv.into()); + let written = encryptor + .encrypt_padded::(&mut out[start..], plaintext.len()) + .map_err(|_| CryptoProviderError::BadInput)? + .len(); + out.truncate(start + written); + Ok(()) + } + + fn aes_256_cbc_decrypt( + &self, + key: &[u8; 32], + iv: &[u8; 16], + ciphertext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError> { + if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) { + return Err(CryptoProviderError::BadInput); + } + + out.clear(); + out.reserve(ciphertext.len()); + out.extend_from_slice(ciphertext); + + let decryptor = cbc::Decryptor::::new(key.into(), iv.into()); + let decrypted_len = decryptor + .decrypt_padded::(out) + .map_err(|_| CryptoProviderError::BadInput)? + .len(); + out.truncate(decrypted_len); + Ok(()) + } + + fn aes_256_gcm_encrypt( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + plaintext: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError> { + let mut enc = + Aes256GcmEncryption::new(key, nonce, aad).map_err(|_| CryptoProviderError::BadInput)?; + let start = out.len(); + out.extend_from_slice(plaintext); + enc.encrypt(&mut out[start..]); + let tag = enc.compute_tag(); + out.extend_from_slice(&tag); + Ok(()) + } + + fn aes_256_gcm_decrypt( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + ciphertext_with_tag: &[u8], + out: &mut Vec, + ) -> Result<(), CryptoProviderError> { + const TAG: usize = 16; + if ciphertext_with_tag.len() < TAG { + return Err(CryptoProviderError::BadInput); + } + let (ct, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - TAG); + + // Decrypt into a scratch, verify tag; only commit to `out` on success + // so failures leave it untouched. + let mut scratch = ct.to_vec(); + let mut dec = + Aes256GcmDecryption::new(key, nonce, aad).map_err(|_| CryptoProviderError::BadInput)?; + dec.decrypt(&mut scratch); + dec.verify_tag(tag) + .map_err(|_| CryptoProviderError::AuthFailed)?; + + out.extend_from_slice(&scratch); + Ok(()) + } + + fn hmac_sha256(&self, key: &[u8], input: &[u8]) -> [u8; 32] { + let mut mac = as KeyInit>::new_from_slice(key) + .expect("HMAC-SHA256 accepts any key length"); + mac.update(input); + mac.finalize().into_bytes().into() + } + + fn aes_256_gcm_encrypt_in_place( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut dyn GcmInPlaceBuffer, + ) -> Result<(), CryptoProviderError> { + let plaintext_len = buffer.len(); + let mut enc = + Aes256GcmEncryption::new(key, nonce, aad).map_err(|_| CryptoProviderError::BadInput)?; + enc.encrypt(buffer.as_mut_slice()); + let tag = enc.compute_tag(); + buffer.resize(plaintext_len + GCM_TAG, 0); + buffer.as_mut_slice()[plaintext_len..].copy_from_slice(&tag); + Ok(()) + } + + fn aes_256_gcm_decrypt_in_place( + &self, + key: &[u8; 32], + nonce: &[u8; 12], + aad: &[u8], + buffer: &mut dyn GcmInPlaceBuffer, + ) -> Result<(), CryptoProviderError> { + let total = buffer.len(); + if total < GCM_TAG { + return Err(CryptoProviderError::BadInput); + } + let pt_len = total - GCM_TAG; + let mut tag = [0u8; GCM_TAG]; + tag.copy_from_slice(&buffer.as_slice()[pt_len..]); + + let mut dec = + Aes256GcmDecryption::new(key, nonce, aad).map_err(|_| CryptoProviderError::BadInput)?; + dec.decrypt(&mut buffer.as_mut_slice()[..pt_len]); + dec.verify_tag(&tag) + .map_err(|_| CryptoProviderError::AuthFailed)?; + buffer.truncate(pt_len); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rust_provider_cbc_roundtrip() { + let p = RustCryptoProvider; + let key = [0x42; 32]; + let iv = [0x11; 16]; + let pt = b"hello cbc provider test"; + + let mut ct = Vec::new(); + p.aes_256_cbc_encrypt(&key, &iv, pt, &mut ct).unwrap(); + + let mut out = Vec::new(); + p.aes_256_cbc_decrypt(&key, &iv, &ct, &mut out).unwrap(); + assert_eq!(&out[..], pt); + } + + #[test] + fn rust_provider_gcm_roundtrip_with_aad() { + let p = RustCryptoProvider; + let key = [0x7f; 32]; + let nonce = [0x13; 12]; + let aad = b"extra auth data"; + let pt = b"hello gcm provider test with aad"; + + let mut sealed = Vec::new(); + p.aes_256_gcm_encrypt(&key, &nonce, aad, pt, &mut sealed) + .unwrap(); + assert_eq!(sealed.len(), pt.len() + 16); + + let mut opened = Vec::new(); + p.aes_256_gcm_decrypt(&key, &nonce, aad, &sealed, &mut opened) + .unwrap(); + assert_eq!(&opened[..], pt); + + // Wrong AAD -> AuthFailed, `out` untouched. + let mut bad = Vec::new(); + let err = p + .aes_256_gcm_decrypt(&key, &nonce, b"different", &sealed, &mut bad) + .unwrap_err(); + assert!(matches!(err, CryptoProviderError::AuthFailed)); + assert!(bad.is_empty()); + } + + #[test] + fn rust_provider_hmac_matches_direct() { + let p = RustCryptoProvider; + let key = b"mac key"; + let data = b"message body"; + let got = p.hmac_sha256(key, data); + + let mut mac = as KeyInit>::new_from_slice(key).unwrap(); + mac.update(data); + let expected: [u8; 32] = mac.finalize().into_bytes().into(); + assert_eq!(got, expected); + } + + /// NIST SP 800-38D Test Case 14: all-zero key/nonce, 128-bit plaintext. + #[test] + fn rust_provider_gcm_nist_tc14() { + let p = RustCryptoProvider; + let key = [0u8; 32]; + let nonce = [0u8; 12]; + let pt = [0u8; 16]; + + let expected_ct: [u8; 16] = [ + 0xce, 0xa7, 0x40, 0x3d, 0x4d, 0x60, 0x6b, 0x6e, 0x07, 0x4e, 0xc5, 0xd3, 0xba, 0xf3, + 0x9d, 0x18, + ]; + let expected_tag: [u8; 16] = [ + 0xd0, 0xd1, 0xc8, 0xa7, 0x99, 0x99, 0x6b, 0xf0, 0x26, 0x5b, 0x98, 0xb5, 0xd4, 0x8a, + 0xb9, 0x19, + ]; + + let mut sealed = Vec::new(); + p.aes_256_gcm_encrypt(&key, &nonce, b"", &pt, &mut sealed) + .unwrap(); + assert_eq!(&sealed[..16], &expected_ct); + assert_eq!(&sealed[16..], &expected_tag); + } +} diff --git a/wacore/libsignal/src/protocol/crypto.rs b/wacore/libsignal/src/protocol/crypto.rs index 3fd17fa71..bffa8f2e3 100644 --- a/wacore/libsignal/src/protocol/crypto.rs +++ b/wacore/libsignal/src/protocol/crypto.rs @@ -3,12 +3,6 @@ // SPDX-License-Identifier: AGPL-3.0-only // -use hmac::{Hmac, KeyInit, Mac}; -use sha2::Sha256; - pub(crate) fn hmac_sha256(key: &[u8], input: &[u8]) -> [u8; 32] { - let mut hmac = - Hmac::::new_from_slice(key).expect("HMAC-SHA256 should accept any size key"); - hmac.update(input); - hmac.finalize().into_bytes().into() + crate::crypto::hmac_sha256(key, input) } diff --git a/wacore/noise/Cargo.toml b/wacore/noise/Cargo.toml index d6d35ec64..decc92602 100644 --- a/wacore/noise/Cargo.toml +++ b/wacore/noise/Cargo.toml @@ -11,7 +11,6 @@ description = "Noise Protocol implementation for WhatsApp with AES-256-GCM" crate-type = ["rlib"] [dependencies] -aes-gcm = { workspace = true } anyhow = { workspace = true } bytes = { workspace = true } hkdf = { workspace = true } diff --git a/wacore/noise/src/lib.rs b/wacore/noise/src/lib.rs index 2380c3977..cd6f704ed 100644 --- a/wacore/noise/src/lib.rs +++ b/wacore/noise/src/lib.rs @@ -38,7 +38,6 @@ pub mod framing; mod handshake; mod state; -pub use aes_gcm::Aes256Gcm; pub use edge_routing::{ EdgeRoutingError, MAX_EDGE_ROUTING_LEN, build_edge_routing_preintro, build_handshake_header, }; diff --git a/wacore/noise/src/state.rs b/wacore/noise/src/state.rs index 1a2fc577d..308624bcb 100644 --- a/wacore/noise/src/state.rs +++ b/wacore/noise/src/state.rs @@ -1,8 +1,15 @@ use crate::error::{NoiseError, Result}; -use aes_gcm::Aes256Gcm; -use aes_gcm::aead::{Aead, AeadInOut, KeyInit, Payload}; use hkdf::Hkdf; use sha2::{Digest, Sha256}; +use wacore_libsignal::crypto::{ + GcmInPlaceBuffer, aes_256_gcm_decrypt, aes_256_gcm_decrypt_in_place, aes_256_gcm_encrypt, + aes_256_gcm_encrypt_in_place, +}; + +/// Buffer kinds accepted by [`NoiseCipher::decrypt_in_place_with_counter`]. +/// Both `Vec` and `bytes::BytesMut` satisfy this via [`GcmInPlaceBuffer`]. +pub trait NoiseBuffer: GcmInPlaceBuffer {} +impl NoiseBuffer for T {} /// Generates an IV (nonce) for AES-GCM from a counter value. /// The counter is placed in the last 4 bytes of a 12-byte IV. @@ -13,72 +20,55 @@ pub fn generate_iv(counter: u32) -> [u8; 12] { iv } +const TAG_LEN: usize = 16; + /// A cipher wrapper that encapsulates AES-256-GCM encryption/decryption /// with counter-based IV generation. -/// -/// This provides a high-level API for post-handshake message encryption -/// without exposing the underlying AES-GCM implementation details. -/// -/// # Example -/// -/// ```ignore -/// use wacore_noise::NoiseCipher; -/// -/// // After handshake, you get read/write ciphers -/// let mut counter = 0u32; -/// -/// // Encrypt with counter -/// let ciphertext = cipher.encrypt_with_counter(counter, plaintext)?; -/// counter = counter.wrapping_add(1); -/// -/// // Decrypt in place with counter -/// cipher.decrypt_in_place_with_counter(counter, &mut ciphertext_buf)?; -/// ``` pub struct NoiseCipher { - inner: Aes256Gcm, + key: [u8; 32], } impl NoiseCipher { /// Creates a new cipher from a 32-byte key. pub fn new(key: &[u8; 32]) -> Result { - let inner = Aes256Gcm::new_from_slice(key) - .map_err(|_| NoiseError::CryptoError("Invalid key size for AES-256-GCM".into()))?; - Ok(Self { inner }) + Ok(Self { key: *key }) } /// Encrypts plaintext using the specified counter for IV generation. - /// /// Returns the ciphertext with appended authentication tag (16 bytes). pub fn encrypt_with_counter(&self, counter: u32, plaintext: &[u8]) -> Result> { let iv = generate_iv(counter); - self.inner - .encrypt((&iv).into(), plaintext) - .map_err(|e| NoiseError::CryptoError(e.to_string())) + let mut out = Vec::with_capacity(plaintext.len() + TAG_LEN); + aes_256_gcm_encrypt(&self.key, &iv, b"", plaintext, &mut out) + .map_err(|e| NoiseError::CryptoError(format!("{e}")))?; + Ok(out) } - /// Encrypts plaintext in-place within the provided buffer. - /// - /// The buffer should contain the plaintext. After encryption, it will - /// contain the ciphertext with the authentication tag appended. - pub fn encrypt_in_place_with_counter(&self, counter: u32, buffer: &mut Vec) -> Result<()> { + /// Encrypts plaintext in-place within the provided buffer: on entry `buffer` + /// holds the plaintext; on return it holds ciphertext + 16-byte tag. + /// Preserves the buffer's allocated capacity across calls. + /// Accepts any [`NoiseBuffer`] (`Vec` or `bytes::BytesMut`). + pub fn encrypt_in_place_with_counter( + &self, + counter: u32, + buffer: &mut B, + ) -> Result<()> { let iv = generate_iv(counter); - self.inner - .encrypt_in_place((&iv).into(), b"", buffer) - .map_err(|e| NoiseError::CryptoError(e.to_string())) + aes_256_gcm_encrypt_in_place(&self.key, &iv, b"", buffer) + .map_err(|e| NoiseError::CryptoError(format!("{e}"))) } - /// Decrypts ciphertext in-place within the provided buffer. - /// - /// The buffer should contain the ciphertext with the 16-byte authentication tag. - /// After decryption, it will contain the plaintext (tag is removed). - pub fn decrypt_in_place_with_counter( + /// Decrypts ciphertext (with 16-byte tag appended) in-place within the + /// provided buffer. On return, `buffer` holds the plaintext (tag removed). + /// Accepts any [`NoiseBuffer`] (`Vec` or `bytes::BytesMut`). + /// Zero allocations with the default [`wacore_libsignal::crypto::RustCryptoProvider`]. + pub fn decrypt_in_place_with_counter( &self, counter: u32, buffer: &mut B, ) -> Result<()> { let iv = generate_iv(counter); - self.inner - .decrypt_in_place((&iv).into(), b"", buffer) + aes_256_gcm_decrypt_in_place(&self.key, &iv, b"", buffer) .map_err(|e| NoiseError::CryptoError(format!("Decrypt failed: {e}"))) } } @@ -98,47 +88,16 @@ fn sha256_digest(data: &[u8]) -> [u8; 32] { } /// The final keys extracted from a completed Noise handshake. -/// -/// Contains `NoiseCipher` instances for both write (outgoing) and read (incoming) -/// directions. Use `encrypt_with_counter` and `decrypt_with_counter` methods -/// with your own counter management. pub struct NoiseKeys { pub write: NoiseCipher, pub read: NoiseCipher, } /// A generic Noise Protocol XX state machine. -/// -/// This implements the core Noise protocol operations without any -/// dependency on specific key agreement libraries. The caller is -/// responsible for computing DH shared secrets externally. -/// -/// # Example -/// -/// ```ignore -/// use wacore_noise::{NoiseState, generate_iv}; -/// -/// // Initialize with pattern and prologue -/// let mut noise = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", &prologue)?; -/// -/// // Authenticate public keys -/// noise.authenticate(&my_ephemeral_public); -/// noise.authenticate(&their_ephemeral_public); -/// -/// // Mix pre-computed shared secret (caller handles DH) -/// noise.mix_key(&shared_secret)?; -/// -/// // Encrypt/decrypt messages -/// let ciphertext = noise.encrypt(plaintext)?; -/// let plaintext = noise.decrypt(ciphertext)?; -/// -/// // Extract final keys -/// let keys = noise.split()?; -/// ``` pub struct NoiseState { hash: [u8; 32], salt: [u8; 32], - cipher: Aes256Gcm, + key: [u8; 32], counter: u32, } @@ -154,11 +113,6 @@ impl NoiseState { } /// Creates a new Noise state with the given pattern and prologue. - /// - /// The pattern should be exactly 32 bytes (used directly as initial hash) - /// or any other length (will be SHA-256 hashed to derive initial state). - /// - /// The prologue is authenticated into the hash state. pub fn new(pattern: impl AsRef<[u8]>, prologue: &[u8]) -> Result { let pattern = pattern.as_ref(); let h: [u8; 32] = if pattern.len() == 32 { @@ -167,13 +121,10 @@ impl NoiseState { sha256_digest(pattern) }; - let cipher = Aes256Gcm::new_from_slice(&h) - .map_err(|_| NoiseError::CryptoError("Invalid key size for AES-256-GCM".into()))?; - let mut state = Self { hash: h, salt: h, - cipher, + key: h, counter: 0, }; @@ -201,40 +152,20 @@ impl NoiseState { /// Encrypts plaintext, updates the hash state with the ciphertext. pub fn encrypt(&mut self, plaintext: &[u8]) -> Result> { let iv = generate_iv(self.post_increment_counter()?); - let payload = Payload { - msg: plaintext, - aad: &self.hash, - }; - let ciphertext = self - .cipher - .encrypt((&iv).into(), payload) - .map_err(|e| NoiseError::CryptoError(e.to_string()))?; - self.authenticate(&ciphertext); - Ok(ciphertext) + let mut out = Vec::with_capacity(plaintext.len() + TAG_LEN); + aes_256_gcm_encrypt(&self.key, &iv, &self.hash, plaintext, &mut out) + .map_err(|e| NoiseError::CryptoError(format!("{e}")))?; + self.authenticate(&out); + Ok(out) } - /// Zero-allocation encryption that appends the ciphertext to the provided buffer. - /// - /// The ciphertext (including the AES-GCM tag) is appended to `out`. - /// The buffer is NOT cleared before appending. + /// Zero-allocation-ish encryption that appends the ciphertext to `out`. pub fn encrypt_into(&mut self, plaintext: &[u8], out: &mut Vec) -> Result<()> { let iv = generate_iv(self.post_increment_counter()?); let aad = self.hash; let start = out.len(); - - // Copy plaintext to output buffer - out.extend_from_slice(plaintext); - - // Encrypt in-place and get the tag separately - let tag = self - .cipher - .encrypt_inout_detached((&iv).into(), &aad, (&mut out[start..]).into()) - .map_err(|e| NoiseError::CryptoError(e.to_string()))?; - - // Append the authentication tag - out.extend_from_slice(&tag); - - // Authenticate with the complete ciphertext (including tag) + aes_256_gcm_encrypt(&self.key, &iv, &aad, plaintext, out) + .map_err(|e| NoiseError::CryptoError(format!("{e}")))?; self.authenticate(&out[start..]); Ok(()) } @@ -243,65 +174,34 @@ impl NoiseState { pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result> { let aad = self.hash; let iv = generate_iv(self.post_increment_counter()?); - let payload = Payload { - msg: ciphertext, - aad: &aad, - }; - let plaintext = self - .cipher - .decrypt((&iv).into(), payload) + let mut out = Vec::with_capacity(ciphertext.len().saturating_sub(TAG_LEN)); + aes_256_gcm_decrypt(&self.key, &iv, &aad, ciphertext, &mut out) .map_err(|e| NoiseError::CryptoError(format!("Noise decrypt failed: {e}")))?; - self.authenticate(ciphertext); - Ok(plaintext) + Ok(out) } /// Zero-allocation decryption that appends the plaintext to the provided buffer. - /// - /// The plaintext is appended to `out`. The buffer is NOT cleared before appending. - /// The ciphertext must include the 16-byte authentication tag. pub fn decrypt_into(&mut self, ciphertext: &[u8], out: &mut Vec) -> Result<()> { - const TAG_LEN: usize = 16; - if ciphertext.len() < TAG_LEN { return Err(NoiseError::CryptoError( "Ciphertext too short (missing tag)".into(), )); } - let aad = self.hash; let iv = generate_iv(self.post_increment_counter()?); - - // Split ciphertext and tag - let (ct, tag_slice) = ciphertext.split_at(ciphertext.len() - TAG_LEN); - let tag: &[u8; TAG_LEN] = tag_slice.try_into().unwrap(); // Safe: we checked length - - let start = out.len(); - - // Copy ciphertext (without tag) to output buffer - out.extend_from_slice(ct); - - // Decrypt in-place - self.cipher - .decrypt_inout_detached((&iv).into(), &aad, (&mut out[start..]).into(), tag.into()) + aes_256_gcm_decrypt(&self.key, &iv, &aad, ciphertext, out) .map_err(|e| NoiseError::CryptoError(format!("Noise decrypt failed: {e}")))?; - - // Authenticate with the original ciphertext (including tag) self.authenticate(ciphertext); Ok(()) } /// Mixes key material into the cipher state (MixKey operation). - /// - /// This is the generic version that accepts pre-computed key material. - /// The caller is responsible for computing DH shared secrets externally - /// using their preferred cryptographic library. pub fn mix_key(&mut self, input_key_material: &[u8]) -> Result<()> { self.counter = 0; let (new_salt, new_key) = self.extract_and_expand(Some(input_key_material))?; self.salt = new_salt; - self.cipher = Aes256Gcm::new_from_slice(&new_key) - .map_err(|_| NoiseError::CryptoError("Invalid key size for AES-256-GCM".into()))?; + self.key = new_key; Ok(()) } @@ -321,9 +221,6 @@ impl NoiseState { } /// Extracts the final write and read keys from the Noise state. - /// - /// This consumes the state and returns `NoiseCipher` instances for - /// subsequent encrypted communication. pub fn split(self) -> Result { let (write_bytes, read_bytes) = self.extract_and_expand(None)?; let write = NoiseCipher::new(&write_bytes)?; @@ -355,7 +252,6 @@ mod tests { let noise = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); - // The hash should have been updated by the prologue assert_ne!(noise.hash(), noise.salt()); } @@ -368,7 +264,6 @@ mod tests { let plaintext = b"hello world"; let ciphertext = noise.encrypt(plaintext).expect("encrypt should succeed"); - // Reset state for decryption (in real use, you'd have two separate states) let mut noise2 = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); @@ -389,9 +284,7 @@ mod tests { .mix_key(&shared_secret) .expect("mix_key should succeed"); - // Salt should have changed assert_ne!(noise.salt(), &old_salt); - // Counter should be reset assert_eq!(noise.counter, 0); } @@ -408,10 +301,8 @@ mod tests { .encrypt_into(plaintext, &mut ciphertext_buf) .expect("encrypt_into should succeed"); - // Verify ciphertext has expected size (plaintext + 16 byte tag) assert_eq!(ciphertext_buf.len(), plaintext.len() + 16); - // Decrypt with fresh state let mut noise2 = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); @@ -428,12 +319,10 @@ mod tests { let prologue = b"test"; let plaintext = b"test message"; - // Test with encrypt() let mut noise1 = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); let ciphertext1 = noise1.encrypt(plaintext).expect("encrypt should succeed"); - // Test with encrypt_into() let mut noise2 = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); let mut ciphertext2 = Vec::new(); @@ -441,10 +330,7 @@ mod tests { .encrypt_into(plaintext, &mut ciphertext2) .expect("encrypt_into should succeed"); - // Both should produce identical ciphertext assert_eq!(ciphertext1, ciphertext2); - - // Both should have same hash state after assert_eq!(noise1.hash(), noise2.hash()); } @@ -456,20 +342,16 @@ mod tests { let plaintext = b"test in-place encryption"; let mut buffer = plaintext.to_vec(); - // Encrypt in-place cipher .encrypt_in_place_with_counter(0, &mut buffer) .expect("encrypt should succeed"); - // Buffer should now be larger (ciphertext + 16 byte tag) assert_eq!(buffer.len(), plaintext.len() + 16); - // Decrypt in-place cipher .decrypt_in_place_with_counter(0, &mut buffer) .expect("decrypt should succeed"); - // Buffer should be back to original plaintext assert_eq!(buffer, plaintext); } @@ -479,10 +361,8 @@ mod tests { let mut noise = NoiseState::new(b"Noise_XX_25519_AESGCM_SHA256\0\0\0\0", prologue) .expect("initialization should succeed"); - // Set counter to max value noise.counter = u32::MAX; - // Next encrypt should fail with CounterExhausted let result = noise.encrypt(b"test"); assert!(matches!(result, Err(NoiseError::CounterExhausted))); } diff --git a/wacore/src/lib.rs b/wacore/src/lib.rs index 5c8969b26..36c00001d 100644 --- a/wacore/src/lib.rs +++ b/wacore/src/lib.rs @@ -1,6 +1,5 @@ extern crate self as wacore; -pub use aes_gcm; pub use wacore_appstate as appstate; pub use wacore_noise as noise; diff --git a/wacore/src/media_retry.rs b/wacore/src/media_retry.rs index 9c52f5b83..bb24a7426 100644 --- a/wacore/src/media_retry.rs +++ b/wacore/src/media_retry.rs @@ -10,8 +10,6 @@ //! Reference: WAWebCryptoMediaRetry, WAWebSendServerErrorReceiptJob, //! WAWebHandleMediaRetryNotification (docs/captured-js/). -use aes_gcm::Aes256Gcm; -use aes_gcm::aead::{Aead, KeyInit, Payload}; use anyhow::{Result, anyhow}; use hkdf::Hkdf; use prost::Message; @@ -20,6 +18,7 @@ use sha2::Sha256; use wacore_binary::Jid; use wacore_binary::builder::NodeBuilder; use wacore_binary::{Node, NodeContentRef, NodeRef}; +use wacore_libsignal::crypto::{aes_256_gcm_decrypt, aes_256_gcm_encrypt}; use waproto::whatsapp as wa; const MEDIA_RETRY_HKDF_INFO: &str = "WhatsApp Media Retry Notification"; @@ -66,8 +65,6 @@ pub fn encrypt_media_retry_receipt( stanza_id: &str, ) -> Result<(Vec, [u8; ENC_IV_SIZE])> { let key = derive_media_retry_key(media_key)?; - let cipher = - Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?; let mut iv = [0u8; ENC_IV_SIZE]; rand::make_rng::().fill_bytes(&mut iv); @@ -77,14 +74,8 @@ pub fn encrypt_media_retry_receipt( }; let plaintext = receipt.encode_to_vec(); - let ciphertext = cipher - .encrypt( - (&iv).into(), - Payload { - msg: &plaintext, - aad: stanza_id.as_bytes(), - }, - ) + let mut ciphertext = Vec::with_capacity(plaintext.len() + 16); + aes_256_gcm_encrypt(&key, &iv, stanza_id.as_bytes(), &plaintext, &mut ciphertext) .map_err(|e| anyhow!("AES-GCM encrypt failed: {e}"))?; Ok((ciphertext, iv)) @@ -100,19 +91,17 @@ pub fn decrypt_media_retry_notification( ciphertext: &[u8], ) -> Result { let key = derive_media_retry_key(media_key)?; - let cipher = - Aes256Gcm::new_from_slice(&key).map_err(|e| anyhow!("AES-GCM key init failed: {e}"))?; - let nonce: &[u8; 12] = iv.try_into().map_err(|_| anyhow!("Invalid IV length"))?; - let plaintext = cipher - .decrypt( - nonce.into(), - Payload { - msg: ciphertext, - aad: stanza_id.as_bytes(), - }, - ) - .map_err(|e| anyhow!("AES-GCM decrypt failed: {e}"))?; + + let mut plaintext = Vec::with_capacity(ciphertext.len().saturating_sub(16)); + aes_256_gcm_decrypt( + &key, + nonce, + stanza_id.as_bytes(), + ciphertext, + &mut plaintext, + ) + .map_err(|e| anyhow!("AES-GCM decrypt failed: {e}"))?; wa::MediaRetryNotification::decode(plaintext.as_slice()) .map_err(|e| anyhow!("protobuf decode failed: {e}")) diff --git a/wacore/src/pair.rs b/wacore/src/pair.rs index 584890197..8a4cc1e07 100644 --- a/wacore/src/pair.rs +++ b/wacore/src/pair.rs @@ -1,6 +1,5 @@ +use crate::libsignal::crypto::aes_256_gcm_encrypt; use crate::libsignal::protocol::{KeyPair, PublicKey}; -use aes_gcm::Aes256Gcm; -use aes_gcm::aead::{Aead, KeyInit, Payload}; use base64::Engine as _; use base64::prelude::*; use hkdf::Hkdf; @@ -321,23 +320,20 @@ impl PairUtils { final_message.extend_from_slice(identity_key.public_key.public_key_bytes()); // Encrypt the final message - let encryption_key = { - let hk = Hkdf::::new(None, &shared_secret); - let mut result = vec![0u8; 32]; - hk.expand(b"WA-Ads-Key", &mut result) - .map_err(|_| anyhow::anyhow!("HKDF expand failed"))?; - result - }; - let cipher = Aes256Gcm::new_from_slice(&encryption_key) - .map_err(|_| anyhow::anyhow!("Invalid key size for AES-GCM"))?; - let nonce: aes_gcm::Nonce<_> = [0u8; 12].into(); - let payload = Payload { - msg: &final_message, - aad: pairing_ref.as_bytes(), - }; - let encrypted = cipher - .encrypt(&nonce, payload) - .map_err(|_| anyhow::anyhow!("AES-GCM encryption failed"))?; + let mut encryption_key = [0u8; 32]; + Hkdf::::new(None, &shared_secret) + .expand(b"WA-Ads-Key", &mut encryption_key) + .map_err(|_| anyhow::anyhow!("HKDF expand failed"))?; + let nonce = [0u8; 12]; + let mut encrypted = Vec::with_capacity(final_message.len() + 16); + aes_256_gcm_encrypt( + &encryption_key, + &nonce, + pairing_ref.as_bytes(), + &final_message, + &mut encrypted, + ) + .map_err(|e| anyhow::anyhow!("AES-GCM encryption failed: {e}"))?; Ok(encrypted) } diff --git a/wacore/src/pair_code.rs b/wacore/src/pair_code.rs index c9dd79b98..bbad5e67d 100644 --- a/wacore/src/pair_code.rs +++ b/wacore/src/pair_code.rs @@ -20,10 +20,9 @@ //! - Bundle encryption: AES-256-GCM after HKDF key derivation use crate::StringEnum; +use crate::libsignal::crypto::aes_256_gcm_encrypt; use crate::libsignal::protocol::{KeyPair, PublicKey}; use aes::cipher::{KeyIvInit, StreamCipher}; -use aes_gcm::Aes256Gcm; -use aes_gcm::aead::{Aead, KeyInit}; use ctr::Ctr128BE; use hkdf::Hkdf; use hmac::{Hmac, Mac}; @@ -468,18 +467,12 @@ impl PairCodeUtils { let mut iv = [0u8; 12]; rand::make_rng::().fill(&mut iv); - // AES-GCM encrypt the bundle - let cipher = Aes256Gcm::new_from_slice(&enc_key) - .map_err(|e| PairCodeError::CryptoError(format!("AES-GCM init failed: {e}")))?; - let encrypted_bundle = cipher - .encrypt((&iv).into(), bundle.as_slice()) - .map_err(|e| PairCodeError::CryptoError(format!("AES-GCM encryption failed: {e}")))?; - // Wrapped bundle = salt (32) + iv (12) + encrypted_bundle (96 + 16 = 112) - let mut wrapped_bundle = Vec::with_capacity(32 + 12 + encrypted_bundle.len()); + let mut wrapped_bundle = Vec::with_capacity(32 + 12 + bundle.len() + 16); wrapped_bundle.extend_from_slice(&key_bundle_salt); wrapped_bundle.extend_from_slice(&iv); - wrapped_bundle.extend_from_slice(&encrypted_bundle); + aes_256_gcm_encrypt(&enc_key, &iv, b"", &bundle, &mut wrapped_bundle) + .map_err(|e| PairCodeError::CryptoError(format!("AES-GCM encryption failed: {e}")))?; Ok((wrapped_bundle, new_adv_secret)) } diff --git a/wacore/src/poll.rs b/wacore/src/poll.rs index 8d455d0aa..64289294e 100644 --- a/wacore/src/poll.rs +++ b/wacore/src/poll.rs @@ -6,7 +6,7 @@ use anyhow::{Result, anyhow}; use hkdf::Hkdf; use sha2::{Digest, Sha256}; -use crate::libsignal::crypto::{Aes256GcmDecryption, Aes256GcmEncryption}; +use crate::libsignal::crypto::{aes_256_gcm_decrypt, aes_256_gcm_encrypt}; const GCM_IV_SIZE: usize = 12; const GCM_TAG_SIZE: usize = 16; @@ -78,12 +78,9 @@ pub fn encrypt_poll_vote( let aad = build_vote_aad(stanza_id, voter_jid); - let mut payload = plaintext; - let mut enc = Aes256GcmEncryption::new(encryption_key, &iv, &aad) - .map_err(|e| anyhow!("AES-GCM init failed: {e}"))?; - enc.encrypt(&mut payload); - let tag = enc.compute_tag(); - payload.extend_from_slice(&tag); + let mut payload = Vec::with_capacity(plaintext.len() + GCM_TAG_SIZE); + aes_256_gcm_encrypt(encryption_key, &iv, &aad, &plaintext, &mut payload) + .map_err(|e| anyhow!("AES-GCM encrypt failed: {e}"))?; Ok((payload, iv)) } @@ -98,12 +95,9 @@ pub fn decrypt_poll_vote( ) -> Result>> { use prost::Message as _; - if iv.len() != GCM_IV_SIZE { - return Err(anyhow!( - "Invalid IV size: expected {GCM_IV_SIZE}, got {}", - iv.len() - )); - } + let nonce: &[u8; GCM_IV_SIZE] = iv + .try_into() + .map_err(|_| anyhow!("Invalid IV size: expected {GCM_IV_SIZE}, got {}", iv.len()))?; if enc_payload.len() < GCM_TAG_SIZE { return Err(anyhow!( @@ -112,14 +106,10 @@ pub fn decrypt_poll_vote( )); } - let (ciphertext, tag) = enc_payload.split_at(enc_payload.len() - GCM_TAG_SIZE); let aad = build_vote_aad(stanza_id, voter_jid); - let mut plaintext = ciphertext.to_vec(); - let mut dec = Aes256GcmDecryption::new(encryption_key, iv, &aad) - .map_err(|e| anyhow!("AES-GCM init failed: {e}"))?; - dec.decrypt(&mut plaintext); - dec.verify_tag(tag) + let mut plaintext = Vec::with_capacity(enc_payload.len().saturating_sub(GCM_TAG_SIZE)); + aes_256_gcm_decrypt(encryption_key, nonce, &aad, enc_payload, &mut plaintext) .map_err(|_| anyhow!("Poll vote GCM tag verification failed"))?; let vote_msg = waproto::whatsapp::message::PollVoteMessage::decode(&plaintext[..])?;