use aes::cipher::{BlockDecryptMut, KeyIvInit};
use aes::Aes128;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use cbc::cipher::block_padding::Pkcs7;
use cbc::Decryptor;
use crate::error::WechatError;
use crate::types::Watermark;
type Aes128CbcDecryptor = Decryptor<Aes128>;
#[non_exhaustive]
#[derive(Debug, Clone, serde::Deserialize)]
pub struct DecryptedUserData {
#[serde(flatten)]
pub data: serde_json::Value,
pub watermark: Watermark,
}
impl DecryptedUserData {
pub fn new(data: serde_json::Value, watermark: Watermark) -> Self {
Self { data, watermark }
}
pub fn open_id(&self) -> Option<&str> {
self.data.get("openId").and_then(|v| v.as_str())
}
pub fn union_id(&self) -> Option<&str> {
self.data.get("unionId").and_then(|v| v.as_str())
}
pub fn nick_name(&self) -> Option<&str> {
self.data.get("nickName").and_then(|v| v.as_str())
}
pub fn phone_number(&self) -> Option<&str> {
self.data
.get("phoneNumber")
.or_else(|| self.data.get("purePhoneNumber"))
.and_then(|v| v.as_str())
}
pub fn country_code(&self) -> Option<&str> {
self.data.get("countryCode").and_then(|v| v.as_str())
}
}
pub fn decrypt_user_data(
session_key: &str,
encrypted_data: &str,
iv: &str,
) -> Result<DecryptedUserData, WechatError> {
let key = BASE64
.decode(session_key)
.map_err(|e| WechatError::Crypto(format!("Invalid session_key: {}", e)))?;
let encrypted = BASE64
.decode(encrypted_data)
.map_err(|e| WechatError::Crypto(format!("Invalid encrypted_data: {}", e)))?;
let iv_bytes = BASE64
.decode(iv)
.map_err(|e| WechatError::Crypto(format!("Invalid iv: {}", e)))?;
if key.len() != 16 {
return Err(WechatError::Crypto(format!(
"Invalid key length: expected 16, got {}",
key.len()
)));
}
if iv_bytes.len() != 16 {
return Err(WechatError::Crypto(format!(
"Invalid IV length: expected 16, got {}",
iv_bytes.len()
)));
}
let decryptor = Aes128CbcDecryptor::new(key.as_slice().into(), iv_bytes.as_slice().into());
let mut buffer = encrypted;
let decrypted = decryptor
.decrypt_padded_mut::<Pkcs7>(&mut buffer)
.map_err(|e| WechatError::Crypto(format!("Decryption failed: {:?}", e)))?;
let json_str = std::str::from_utf8(decrypted)
.map_err(|e| WechatError::Crypto(format!("Invalid UTF-8: {}", e)))?;
let user_data: DecryptedUserData = serde_json::from_str(json_str)
.map_err(|e| WechatError::Crypto(format!("Invalid JSON: {}", e)))?;
Ok(user_data)
}
pub fn verify_watermark(data: &DecryptedUserData, expected_appid: &str) -> Result<(), WechatError> {
if data.watermark.appid() != expected_appid {
return Err(WechatError::Signature(format!(
"Watermark appid mismatch: expected {}, got {}",
expected_appid,
data.watermark.appid()
)));
}
Ok(())
}
pub fn verify_watermark_with_max_skew(
data: &DecryptedUserData,
expected_appid: &str,
now_timestamp: i64,
max_skew_seconds: i64,
) -> Result<(), WechatError> {
verify_watermark(data, expected_appid)?;
data.watermark
.verify_timestamp_freshness(now_timestamp, max_skew_seconds)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_invalid_base64_session_key() {
let result = decrypt_user_data("not-valid-base64!!!", "data", "iv");
assert!(result.is_err());
}
#[test]
fn test_invalid_base64_encrypted_data() {
let result = decrypt_user_data("MTIzNDU2Nzg5MDEyMzQ1Ng==", "not-valid!!!", "iv");
assert!(result.is_err());
}
#[test]
fn test_verify_watermark_with_max_skew_success() {
let data = DecryptedUserData::new(
serde_json::json!({ "openId": "o123" }),
Watermark::new(1_700_000_000, "wx1234567890abcdef"),
);
let result =
verify_watermark_with_max_skew(&data, "wx1234567890abcdef", 1_700_000_120, 180);
assert!(result.is_ok());
}
#[test]
fn test_verify_watermark_with_max_skew_rejects_stale() {
let data = DecryptedUserData::new(
serde_json::json!({ "openId": "o123" }),
Watermark::new(1_700_000_000, "wx1234567890abcdef"),
);
let result =
verify_watermark_with_max_skew(&data, "wx1234567890abcdef", 1_700_000_301, 300);
assert!(result.is_err());
}
}