zeroclaw-labs/zeroclaw · error · anyhow::Error
media aes_key must decode to 16 raw bytes or 32 hex chars, g
Error message
media aes_key must decode to 16 raw bytes or 32 hex chars, got {} bytes What it means
parse_aes_key in the WeChat channel decodes the aes_key attached to an inbound media message before AES-128-ECB decrypting the file. It accepts exactly three shapes: 32 ASCII hex chars; base64 decoding to 16 raw bytes; or base64 of 32 ASCII hex chars (nested hex). This error fires when the base64 decode succeeded but produced a byte length that is neither 16 nor 32-hex — so the key arrived, but in a malformed or unexpected encoding.
Source
Thrown at crates/zeroclaw-channels/src/wechat.rs:424
"media nested hex aes_key invalid"
);
anyhow::Error::msg(format!("media nested hex aes_key invalid: {e}"))
})?;
return <[u8; 16]>::try_from(bytes.as_slice()).map_err(|_| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(
::serde_json::json!({"key_kind": "nested_hex", "expected_bytes": 16})
),
"wechat: media nested hex aes_key has wrong byte length"
);
anyhow::Error::msg("media nested hex aes_key must be 16 bytes")
});
}
anyhow::bail!(
"media aes_key must decode to 16 raw bytes or 32 hex chars, got {} bytes",
decoded.len()
)
}
fn https_base_url(
field_name: &str,
value: Option<String>,
default: &str,
) -> anyhow::Result<String> {
let url = value.unwrap_or_else(|| default.to_string());
let url = url.trim().trim_end_matches('/').to_string();
if !url.starts_with("https://") {
anyhow::bail!("{field_name} must use https://, got {url}");
}
Ok(url)
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Dump the raw aes_key string from the inbound event and check its length: 32 chars hex, or base64 that decodes to exactly 16 bytes.
- Confirm the value is taken verbatim from the attachment payload (no quotes, escaping, or trimming artifacts introduced by your pipeline).
- If WeChat/iLink changed the encoding shape, update the channel to map the new format to [u8; 16].
- For test payloads, generate a real key: openssl rand -hex 16 (32 hex chars) — never paste a raw 24/32-byte string.
Example fix
// before — placeholder key of the wrong length
let key = parse_aes_key("YWJjZGVmZ2hpamtsbW5vcA==")?; // base64 -> 16 bytes? no: 17 bytes -> error
// after — supply a genuine 128-bit key in one of the accepted shapes
let hex_key = "5a1f2b3c4d5e6f708192a3b4c5d6e7f8"; // 32 hex chars
let key = parse_aes_key(hex_key)?; // Ok([u8; 16]) Defensive patterns
Strategy: validation
Validate before calling
// Validate an inbound aes_key before attempting the attachment download.
fn aes_key_shape_ok(raw: &str) -> bool {
let t = raw.trim();
if t.len() == 32 && t.bytes().all(|b| b.is_ascii_hexdigit()) {
return true; // 32 hex chars
}
match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, t) {
Ok(bytes) => bytes.len() == 16
|| (bytes.len() == 32 && bytes.iter().all(u8::is_ascii_hexdigit)),
Err(_) => false,
}
}
anyhow::ensure!(aes_key_shape_ok(&event.aes_key), "malformed aes_key: wrong length/encoding"); Try / catch
match channel.download_inbound_attachment(&event).await {
Ok(bytes) => { /* persist media */ }
Err(e) if e.to_string().contains("media aes_key must decode to") => {
// Upstream payload problem, not transient: log the key length/encoding,
// skip this attachment, and never retry the identical payload.
}
Err(e) => return Err(e),
} Prevention
- Treat the event's aes_key as opaque — pass it through verbatim without trimming/re-escaping beyond the channel's own handling.
- Build test fixtures with openssl rand -hex 16 so keys always have a valid shape.
- Log key length (never the key) on parse failure to detect upstream format changes quickly.
- Pin the WeChat/iLink API behavior in integration tests so an upstream key-encoding change fails loudly in CI.
When it happens
Trigger: download_inbound_attachment receives an event whose aes_key (1) was copied/serialized with extra characters so base64 decodes to 17/24/33... bytes; (2) is base64 of a 16-byte key with padding or whitespace mangled by upstream JSON handling; (3) comes from a WeChat iLink API change that altered key encoding (e.g. raw bytes length different than 16, or a new wrapper). Every length-check branch above logs a WARN with key_kind before this fallthrough bail.
Common situations: WeChat backend rolling out a new media-message format while older channel code parses it; a proxy/transformer in the event pipeline re-encoding the key; hand-crafted test payloads using a 24-byte (192-bit) or 32-byte raw key instead of the required 128-bit key; config/tutorial examples showing a placeholder key of the wrong length.
Related errors
- inbound attachment exceeds {} MB limit
- Cannot persist empty {channel_type} identity
- modal custom_id exceeds Discord's 100-char limit; cannot ope
- slash command registration failed for '{name}' ({status}): {
- unknown git channel provider `{other}` (supported: github, g
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/0956ef1b98f4e549.
Report an issue: GitHub.