use crate::handlers::pxcrypto;
use crate::modules::pxutils;
use crate::px_debug;
use crate::pxconfig::PXConfig;
use crate::pxcontext::{
BlockReason, CallReason, CookieOrigin, CookieVersion, PXContext, PassReason, TokenVersion,
VidSource,
};
use base64::{Engine as _, engine::general_purpose};
use regex::Regex;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
fn parse_cookie_v2(cookie_json: &serde_json::Value, ctx: &mut PXContext) -> CallReason {
let re = match Regex::new(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
) {
Ok(r) => r,
Err(_) => return CallReason::CookieDecryptionFailed,
};
if *cookie_json != serde_json::json!({}) {
if cookie_json["t"].is_null()
|| cookie_json["h"].is_null()
|| cookie_json["u"].is_null()
|| cookie_json["v"].is_null()
{
px_debug!("Decoded cookie is invalid, value: {}", cookie_json);
return CallReason::CookieDecryptionFailed;
}
let vid = cookie_json["v"].as_str().unwrap_or_default();
let uuid = cookie_json["u"].as_str().unwrap_or_default();
let hash = cookie_json["h"].as_str().unwrap_or_default();
if !vid.is_empty() {
ctx.vid = Some(vid.to_string());
ctx.vid_source = Some(VidSource::RiskCookie);
}
ctx.uuid = Some(uuid.to_string());
ctx.v2_cookie_hash = Some(hash.to_string());
if !re.is_match(uuid) || !re.is_match(vid) {
px_debug!("Cookie UUID/VID validation failed, value: {}", cookie_json);
return CallReason::CookieDecryptionFailed;
}
CallReason::None
} else {
CallReason::CookieDecryptionFailed
}
}
fn validate_original_token_v2(payload: &str, ctx: &mut PXContext, conf: &PXConfig) -> CallReason {
let original_token_json = pxutils::get_cookie_json(payload);
let token_pass_data_hex =
pxcrypto::get_cookie_hmac(&original_token_json, "0", &conf.cookie_secret);
let token_block_data_hex =
pxcrypto::get_cookie_hmac(&original_token_json, "1", &conf.cookie_secret);
let hash = original_token_json
.get("h")
.and_then(|v| v.as_str())
.unwrap_or_default();
if hash != token_pass_data_hex.unwrap_or_default()
&& hash != token_block_data_hex.unwrap_or_default()
{
CallReason::CookieValidationFailed
} else {
parse_cookie_v2(&original_token_json, ctx)
}
}
fn validate_cookie_v3_schema(cookie_json: &serde_json::Value) -> CallReason {
if cookie_json.get("v").is_none()
|| cookie_json.get("u").is_none()
|| cookie_json.get("s").is_none()
|| cookie_json.get("t").is_none()
|| cookie_json.get("a").is_none()
{
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
return CallReason::CookieDecryptionFailed;
}
if cookie_json.get("t").and_then(|t| t.as_i64()).is_none() {
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
return CallReason::CookieDecryptionFailed;
}
CallReason::None
}
fn validate_original_token_v3(payload: &str, ctx: &PXContext, conf: &PXConfig) -> CallReason {
let (cookie_json, ehmac, cookie_to_sign) = match decrypt_versioned_cookie_v3(payload, conf) {
Ok(parts) => parts,
Err(reason) => return reason,
};
let schema_result = validate_cookie_v3_schema(&cookie_json);
if schema_result != CallReason::None {
return schema_result;
}
let t = cookie_json
.get("t")
.and_then(|v| v.as_i64())
.unwrap_or_default();
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
if current_timestamp > t {
return CallReason::CookieExpired;
}
let signing_fields = get_cookie_v3_signing_fields(cookie_json, ctx, conf);
if !digest_cookie_v3(cookie_to_sign, ehmac, signing_fields, conf) {
px_debug!("Original token v3 HMAC validation failed");
return CallReason::CookieValidationFailed;
}
CallReason::None
}
fn validate_original_token(ctx: &mut PXContext, conf: &PXConfig) -> Option<CallReason> {
let original_token = ctx.original_token.clone().filter(|v| !v.is_empty())?;
let reason = match pxutils::parse_versioned_mobile_token(&original_token) {
Some((cookie_name, payload)) if cookie_name == "_px2" => {
validate_original_token_v2(&payload, ctx, conf)
}
Some((cookie_name, payload)) if cookie_name == "_px3" => {
validate_original_token_v3(&payload, ctx, conf)
}
Some(_) => CallReason::CookieValidationFailed,
None => validate_original_token_v2(&original_token, ctx, conf),
};
Some(reason)
}
fn verify_mobile_sdk_error(ctx: &mut PXContext, conf: &PXConfig) -> bool {
if ctx.original_token.as_ref().is_some_and(|v| !v.is_empty()) {
px_debug!("Original token found, Evaluating.");
if let Some(original_token_error) = validate_original_token(ctx, conf) {
ctx.original_token_error = match original_token_error {
CallReason::None => None,
other => Some(other),
};
}
}
false
}
pub fn verify_cookie_v2(ctx: &mut PXContext, conf: &PXConfig) -> bool {
let px_cookie = match ctx.cookies.get("_px2") {
Some(c) => c.clone(),
None => {
ctx.s2s_call_reason = Some(CallReason::NoCookie);
return false;
}
};
let cookie_json = pxutils::get_cookie_json(&px_cookie);
ctx.decoded_v2_cookie = Some(cookie_json.to_string());
let parsed = parse_cookie_v2(&cookie_json, ctx);
ctx.s2s_call_reason = match parsed {
CallReason::None => None,
other => Some(other),
};
if ctx.s2s_call_reason.is_some() {
return false;
}
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
if current_timestamp
> cookie_json
.get("t")
.and_then(|v| v.as_i64())
.unwrap_or_default()
{
ctx.s2s_call_reason = Some(CallReason::CookieExpired);
return false;
}
let mut pass_cookie_param = "0".into();
if ctx.cookie_origin == Some(CookieOrigin::Cookie) {
pass_cookie_param = format!("{}{}", "0", ctx.user_agent);
}
let pass_data_hex =
pxcrypto::get_cookie_hmac(&cookie_json, &pass_cookie_param, &conf.cookie_secret);
if ctx.v2_cookie_hash.as_deref().unwrap_or_default() == pass_data_hex.unwrap_or_default() {
if ctx.is_sensitive_route {
ctx.s2s_call_reason = Some(CallReason::SensitiveRoute);
return false;
}
px_debug!("Valid request by cookie - pass");
ctx.pass_reason = Some(PassReason::Cookie);
return true;
}
let mut block_cookie_param = "1".into();
if ctx.cookie_origin == Some(CookieOrigin::Cookie) {
block_cookie_param = format!("{}{}", "1", ctx.user_agent);
}
let block_data_hex =
pxcrypto::get_cookie_hmac(&cookie_json, &block_cookie_param, &conf.cookie_secret);
if ctx.v2_cookie_hash.as_deref().unwrap_or_default() == block_data_hex.unwrap_or_default() {
px_debug!("Request blocked by cookie");
ctx.block_reason = Some(BlockReason::CookieScore);
return true;
}
ctx.s2s_call_reason = Some(CallReason::CookieValidationFailed);
false
}
fn decrypt_versioned_cookie_v3(
px_cookie: &str,
conf: &PXConfig,
) -> Result<(serde_json::Value, String, String), CallReason> {
let fields = px_cookie.split(':').collect::<Vec<&str>>();
if fields.len() != 4 {
px_debug!("Cookie decryption failed, value: {}", px_cookie);
return Err(CallReason::CookieDecryptionFailed);
}
let cookie_to_sign = fields.get(1..).map(|f| f.join(":")).unwrap_or_default();
let ehmac = match fields.first() {
Some(hmac) => hmac.to_string(),
None => return Err(CallReason::CookieDecryptionFailed),
};
let salt = match fields
.get(1)
.and_then(|s| general_purpose::STANDARD.decode(s).ok())
{
Some(s) => s,
None => {
px_debug!("Cookie decryption failed, value: {}", px_cookie);
return Err(CallReason::CookieDecryptionFailed);
}
};
let iterations: usize = match fields.get(2).and_then(|s| FromStr::from_str(s).ok()) {
Some(u) => u,
None => {
px_debug!("Cookie decryption failed, value: {}", px_cookie);
return Err(CallReason::CookieDecryptionFailed);
}
};
if iterations > conf.risk_cookie_max_iterations || iterations < conf.risk_cookie_min_iterations
{
px_debug!(
"Cookie v3 decryption failed, iterations: {} max: {} min: {}",
iterations,
conf.risk_cookie_max_iterations,
conf.risk_cookie_min_iterations
);
return Err(CallReason::CookieDecryptionFailed);
}
let mut payload = match fields
.get(3)
.and_then(|s| general_purpose::STANDARD.decode(s.as_bytes()).ok())
{
Some(p) => p,
None => {
px_debug!("Cookie v3 decryption failed, value: {}", px_cookie);
return Err(CallReason::CookieDecryptionFailed);
}
};
let dec_payload =
match pxcrypto::decrypt_cookie_v3(&conf.cookie_secret, salt, iterations, &mut payload) {
Some(d) => {
px_debug!("Cookie v3 decrypted successfully");
d
}
None => {
px_debug!("Cookie v3 decryption failed");
return Err(CallReason::CookieDecryptionFailed);
}
};
let cookie_json: serde_json::Value = match serde_json::from_slice(dec_payload) {
Ok(j) => j,
Err(e) => {
px_debug!("Cookie v3 validation failed: {}", e);
return Err(CallReason::CookieValidationFailed);
}
};
Ok((cookie_json, ehmac, cookie_to_sign))
}
fn decrypt_cookie_v3(
ctx: &mut PXContext,
conf: &PXConfig,
) -> Option<(serde_json::Value, String, String)> {
let px_cookie = ctx.cookies.get("_px3")?.clone();
match decrypt_versioned_cookie_v3(&px_cookie, conf) {
Ok(parts) => Some(parts),
Err(reason) => {
ctx.s2s_call_reason = Some(reason);
None
}
}
}
fn get_cookie_v3_signing_fields(
cookie_json: serde_json::Value,
ctx: &PXContext,
conf: &PXConfig,
) -> String {
let mut out: String = String::from("");
let x = match cookie_json.get("x") {
Some(x) => x.as_str().unwrap_or_default(),
None => return out,
};
let ua = if ctx.cookie_origin == Some(CookieOrigin::Header) {
"".to_string()
} else if ctx.user_agent.len() > conf.user_agent_max_length {
ctx.user_agent
.split_at(conf.user_agent_max_length)
.0
.to_string()
} else {
ctx.user_agent.clone()
};
for field in x.chars() {
match field {
'u' => out += &ua,
's' => out += &ctx.ip,
_ => (),
}
}
out
}
fn digest_cookie_v3(
cookie_to_sign: String,
ehmac: String,
signing_fields: String,
conf: &PXConfig,
) -> bool {
let to_sign = cookie_to_sign + &signing_fields;
let hmac = pxcrypto::create_hmac(&to_sign, &conf.cookie_secret);
hmac.unwrap_or_default() == ehmac
}
fn verify_cookie_v3(ctx: &mut PXContext, conf: &PXConfig) -> bool {
let (cookie_json, ehmac, cookie_to_sign) = match decrypt_cookie_v3(ctx, conf) {
Some(j) => j,
None => {
return false;
}
};
ctx.cookie_json = Some(cookie_json.clone().to_string());
let schema_result = validate_cookie_v3_schema(&cookie_json);
if schema_result != CallReason::None {
ctx.s2s_call_reason = Some(schema_result);
return false;
}
let t = cookie_json
.get("t")
.and_then(|v| v.as_i64())
.unwrap_or_default();
ctx.score = match cookie_json.get("s") {
Some(s) => Some(s.as_u64().unwrap_or_default() as u8),
None => {
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
ctx.s2s_call_reason = Some(CallReason::CookieDecryptionFailed);
return false;
}
};
ctx.uuid = match cookie_json.get("u") {
Some(u) => Some(u.as_str().unwrap_or_default().to_string()),
None => {
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
ctx.s2s_call_reason = Some(CallReason::CookieDecryptionFailed);
return false;
}
};
ctx.vid = match cookie_json.get("v") {
Some(v) => Some(v.as_str().unwrap_or_default().to_string()),
None => {
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
ctx.s2s_call_reason = Some(CallReason::CookieDecryptionFailed);
return false;
}
};
if ctx.vid.as_ref().is_some_and(|v| !v.is_empty()) {
ctx.vid_source = Some(VidSource::RiskCookie);
}
ctx.block_action = match cookie_json.get("a") {
Some(v) => Some(v.as_str().unwrap_or_default().to_string()),
None => {
px_debug!("Decoded cookie v3 is invalid, value: {}", cookie_json);
ctx.s2s_call_reason = Some(CallReason::CookieDecryptionFailed);
return false;
}
};
ctx.additional_token_info = cookie_json
.get("add")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let current_timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
if current_timestamp > t {
ctx.s2s_call_reason = Some(CallReason::CookieExpired);
px_debug!(
"Cookie TTL is expired, value: {} age: {}",
current_timestamp,
current_timestamp - t
);
return false;
}
let is_highscore = ctx.score.unwrap_or(0) >= conf.blocking_score;
if is_highscore {
px_debug!(
"Cookie evaluation ended successfully, risk score: {:?}",
ctx.score
);
ctx.s2s_call_reason = None;
ctx.block_reason = Some(BlockReason::CookieScore);
return false;
}
let signing_fields = get_cookie_v3_signing_fields(cookie_json, ctx, conf);
let digest_ok = digest_cookie_v3(cookie_to_sign, ehmac, signing_fields, conf);
if !digest_ok {
px_debug!("Cookie v3 HMAC validation failed");
ctx.s2s_call_reason = Some(CallReason::CookieValidationFailed);
return false;
}
if ctx.is_sensitive_route {
ctx.s2s_call_reason = Some(CallReason::SensitiveRoute);
return false;
}
px_debug!("Valid request by cookie v3 - pass");
ctx.pass_reason = Some(PassReason::Cookie);
true
}
pub fn verify_cookie(ctx: &mut PXContext, conf: &PXConfig) -> bool {
if ctx.cookie_origin == Some(CookieOrigin::Header)
&& ctx
.s2s_call_reason
.as_ref()
.is_some_and(|r| r.is_mobile_sdk_error())
{
return verify_mobile_sdk_error(ctx, conf);
}
match conf.token_version {
TokenVersion::V3 => {
if ctx.cookies.get("_px3").is_some_and(|c| !c.is_empty()) {
ctx.cookie_version = Some(CookieVersion::V3);
return verify_cookie_v3(ctx, conf);
}
}
TokenVersion::V2 => {
if ctx.cookies.get("_px2").is_some_and(|c| !c.is_empty()) {
ctx.cookie_version = Some(CookieVersion::V2);
return verify_cookie_v2(ctx, conf);
}
}
}
if ctx.pxhd_cookie.as_ref().is_some_and(|v| !v.is_empty()) {
ctx.s2s_call_reason = Some(CallReason::NoCookieWVid);
} else {
ctx.s2s_call_reason = Some(CallReason::NoCookie);
}
false
}