{"record":{"id":"10a8dc4bf3bef98e","repo":"EpicGames/lore","slug":"presigned-url-hmac-key-is-not-valid-hex-e","errorCode":null,"errorMessage":"presigned_url_hmac_key is not valid hex: {e}","messagePattern":"presigned_url_hmac_key is not valid hex: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"lore-server/src/http/server.rs","lineNumber":204,"sourceCode":"    }\n}\n\n/// Renders the resolved allowlist for the startup log.\nfn describe_allowed_types(types: &[String]) -> String {\n    if types.is_empty() {\n        \"<none>\".to_string()\n    } else {\n        types.join(\", \")\n    }\n}\n\nfn build_presign_config(settings: &PresignSettings) -> Result<Option<PresignConfig>> {\n    let Some(key_hex) = settings.hmac_key.as_deref() else {\n        return Ok(None);\n    };\n\n    let key_bytes = hex::decode(key_hex)\n        .map_err(|e| anyhow::anyhow!(\"presigned_url_hmac_key is not valid hex: {e}\"))?;\n\n    if key_bytes.len() < MIN_HMAC_KEY_BYTES {\n        anyhow::bail!(\n            \"presigned_url_hmac_key must be at least {MIN_HMAC_KEY_BYTES} bytes, got {}\",\n            key_bytes.len()\n        );\n    }\n\n    let key_id = blake3::hash(&key_bytes).to_hex()[..16].to_string();\n    let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, &key_bytes);\n\n    Ok(Some(PresignConfig {\n        hmac_key,\n        key_id,\n        min_ttl_seconds: settings.min_ttl_seconds,\n        default_ttl_seconds: settings.default_ttl_seconds,\n        max_ttl_seconds: settings.max_ttl_seconds,\n        content_type_allowlist: ContentTypeAllowlist::try_from_policy(","sourceCodeStart":186,"sourceCodeEnd":222,"githubUrl":"https://github.com/EpicGames/lore/blob/074eb0b0d1194c997d7cf28b55519e3e197b3e23/lore-server/src/http/server.rs#L186-L222","documentation":"At startup, build_presign_config reads settings.presigned_url_hmac_key, which must be a hex-encoded HMAC key for signing S3 presigned URLs. If hex::decode fails on that string, the server aborts with this message. The library validates eagerly so bad signing keys never reach request handling.","triggerScenarios":"Setting presigned_url_hmac_key in the server settings (config file or env) to a value that is not valid hex: odd number of characters, whitespace, '0x' prefix, non-hex characters (g, z, -), or an empty-but-present string.","commonSituations":"Operators paste a base64 or raw ASCII secret into the config instead of hex; a shell variable carries a trailing newline or quotes; someone prefixes the key with '0x'; YAML/JSON mangling introduces spaces.","solutions":["Hex-encode the key: generate with `openssl rand -hex 32` and set the result verbatim.","Strip whitespace/quotes and remove any '0x' prefix from the configured value.","If the key was stored base64-encoded, convert with `base64 -d | xxd -p -c 64`.","If presigned URLs are not needed, unset the key entirely (build_presign_config returns Ok(None))."],"exampleFix":"// before (config)\npresigned_url_hmac_key = \"0xdeadbeef\"\n// after\npresigned_url_hmac_key = \"deadbeef\" // valid hex, >= 32 bytes","handlingStrategy":"validation","validationCode":"fn valid_hex_key(key: &str, min_bytes: usize) -> Result<(), String> {\n    let k = key.trim().trim_start_matches(\"0x\");\n    hex::decode(k)\n        .map_err(|e| format!(\"not valid hex: {e}\"))?\n        .len()\n        .ge(&min_bytes)\n        .then_some(())\n        .ok_or_else(|| format!(\"key shorter than {min_bytes} bytes\"))\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Generate keys with `openssl rand -hex 32` and store them already hex-encoded.","Validate all secret env vars in CI or an entrypoint pre-flight script.","Trim whitespace and strip quotes before persisting the value into config."],"tags":["config","hex","hmac","startup"],"backgroundTag":"invalid-config-value","analyzedSha":"074eb0b0d1194c997d7cf28b55519e3e197b3e23","analyzedAt":"2026-09-13T09:00:57.509Z","contentChangedAt":"2026-09-13T09:00:57.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}