use aes::Aes256;
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use cfb_mode::cipher::{AsyncStreamCipher, KeyIvInit};
use serde::Deserialize;
use tracing::warn;
use super::client::{Interaction, InteractionProtocol, InteractshError};
type Aes256CfbDec = cfb_mode::Decryptor<Aes256>;
#[derive(Deserialize, Default)]
#[serde(default)]
struct InteractionRaw {
protocol: String,
#[serde(rename = "unique-id")]
unique_id: String,
#[serde(rename = "full-id")]
full_id: String,
#[serde(rename = "remote-address")]
remote_address: String,
timestamp: String,
#[serde(rename = "raw-request")]
raw_request: String,
#[serde(rename = "raw-response")]
raw_response: String,
#[serde(rename = "q-type")]
q_type: String,
}
const MAX_RAW_PAYLOAD: usize = 16 * 1024;
const AES_CFB_IV_LEN: usize = 16;
pub(super) fn decrypt_entry(
aes_key: &[u8],
b64: &str,
) -> Result<Option<Interaction>, InteractshError> {
let mut bytes = B64
.decode(b64.as_bytes())
.map_err(|e| InteractshError::Decrypt(format!("base64: {e}")))?;
if bytes.len() < AES_CFB_IV_LEN {
return Err(InteractshError::Decrypt(format!(
"ciphertext too short ({} < {AES_CFB_IV_LEN})",
bytes.len()
)));
}
let (iv, payload) = bytes.split_at_mut(AES_CFB_IV_LEN);
Aes256CfbDec::new_from_slices(aes_key, iv)
.map_err(|e| InteractshError::Decrypt(format!("cfb init: {e}")))?
.decrypt(payload);
let json = match std::str::from_utf8(payload) {
Ok(s) => s,
Err(e) => {
warn!(
target: "keyhog::oob",
error = %e,
"interactsh entry decrypted to non-UTF-8 bytes (wrong AES key or \
corrupt ciphertext); skipping this interaction, an OOB callback \
may be missed"
);
return Ok(None);
}
};
let raw: InteractionRaw = match serde_json::from_str(json) {
Ok(v) => v,
Err(e) => {
warn!(
target: "keyhog::oob",
error = %e,
"interactsh JSON parse failed; skipping this interaction, an OOB \
callback may be missed"
);
return Ok(None);
}
};
let unique_id = if !raw.full_id.is_empty() {
raw.full_id
} else {
raw.unique_id
};
if unique_id.is_empty() {
warn!(
target: "keyhog::oob",
"interactsh interaction carried no full-id or unique-id; skipping this \
interaction: an OOB callback may be missed"
);
return Ok(None);
}
let raw_payload = if !raw.raw_request.is_empty() {
raw.raw_request
} else if !raw.raw_response.is_empty() {
raw.raw_response
} else {
raw.q_type
};
let raw_payload = truncate_raw_payload(raw_payload);
Ok(Some(Interaction {
unique_id,
protocol: InteractionProtocol::parse(&raw.protocol),
remote_address: raw.remote_address,
timestamp: raw.timestamp,
raw_payload,
}))
}
fn truncate_raw_payload(mut raw_payload: String) -> String {
if raw_payload.len() > MAX_RAW_PAYLOAD {
let mut idx = MAX_RAW_PAYLOAD;
while idx > 0 && !raw_payload.is_char_boundary(idx) {
idx -= 1;
}
raw_payload.truncate(idx);
}
raw_payload
}