use base64::{Engine, engine::general_purpose::STANDARD};
use kawa::{Block, Pair, Store};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use super::GenericHttpStream;
use crate::protocol::http::parser::compare_no_case;
const DEFAULT_MAX_DECODED_CREDENTIAL_BYTES: usize = 4096;
static MAX_DECODED_CREDENTIAL_BYTES_OVERRIDE: std::sync::OnceLock<usize> =
std::sync::OnceLock::new();
pub fn set_max_decoded_credential_bytes(cap: usize) {
if cap == 0 {
return;
}
let _ = MAX_DECODED_CREDENTIAL_BYTES_OVERRIDE.set(cap);
}
fn max_decoded_credential_bytes() -> usize {
MAX_DECODED_CREDENTIAL_BYTES_OVERRIDE
.get()
.copied()
.unwrap_or(DEFAULT_MAX_DECODED_CREDENTIAL_BYTES)
}
pub fn extract_authorization_header(kawa: &GenericHttpStream) -> Option<Vec<u8>> {
let buf = kawa.storage.buffer();
for block in &kawa.blocks {
if let Block::Header(Pair { key, val }) = block {
if matches!(key, Store::Empty) {
continue;
}
let key_bytes = key.data(buf);
if compare_no_case(key_bytes, b"authorization") {
return Some(val.data(buf).to_vec());
}
}
}
None
}
pub fn canonicalize_basic_credentials(value: &[u8]) -> Option<String> {
let mut rest = value;
while let Some((&b' ', tail)) = rest.split_first() {
rest = tail;
}
let scheme_len = b"Basic".len();
if rest.len() < scheme_len || !compare_no_case(&rest[..scheme_len], b"basic") {
return None;
}
rest = &rest[scheme_len..];
let mut saw_space = false;
while let Some((&b' ', tail)) = rest.split_first() {
rest = tail;
saw_space = true;
}
if !saw_space || rest.is_empty() {
return None;
}
let max_decoded = max_decoded_credential_bytes();
let max_encoded = max_decoded.saturating_mul(4).saturating_add(2) / 3 + 4;
if rest.len() > max_encoded {
return None;
}
let decoded = STANDARD.decode(rest).ok()?;
if decoded.len() > max_decoded {
return None;
}
let colon = decoded.iter().position(|&b| b == b':')?;
let username = std::str::from_utf8(&decoded[..colon]).ok()?;
let password = &decoded[colon + 1..];
let mut hasher = Sha256::new();
hasher.update(password);
let digest = hasher.finalize();
Some(format!("{}:{}", username, hex::encode(digest)))
}
const AUTH_COMPARE_PAD_LEN: usize = 256;
fn pad_for_constant_time_compare(input: &[u8]) -> [u8; AUTH_COMPARE_PAD_LEN + 8] {
let mut buf = [0u8; AUTH_COMPARE_PAD_LEN + 8];
let n = input.len().min(AUTH_COMPARE_PAD_LEN);
buf[..n].copy_from_slice(&input[..n]);
buf[AUTH_COMPARE_PAD_LEN..].copy_from_slice(&(input.len() as u64).to_le_bytes());
buf
}
pub fn check_authorized_hashes(candidate: &str, authorized_hashes: &[String]) -> bool {
if candidate.len() > AUTH_COMPARE_PAD_LEN {
return false;
}
let candidate_padded = pad_for_constant_time_compare(candidate.as_bytes());
let mut matched = subtle::Choice::from(0u8);
for hash in authorized_hashes {
if hash.len() > AUTH_COMPARE_PAD_LEN {
continue;
}
let entry_padded = pad_for_constant_time_compare(hash.as_bytes());
matched |= candidate_padded.as_slice().ct_eq(entry_padded.as_slice());
}
bool::from(matched)
}
pub fn check_basic(kawa: &GenericHttpStream, authorized_hashes: &[String]) -> bool {
if authorized_hashes.is_empty() {
return false;
}
let Some(header_value) = extract_authorization_header(kawa) else {
return false;
};
let Some(canonical) = canonicalize_basic_credentials(&header_value) else {
return false;
};
check_authorized_hashes(&canonical, authorized_hashes)
}
#[cfg(test)]
mod tests {
use super::*;
const SECRET_SHA256_HEX: &str =
"2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b";
#[test]
fn canonicalize_round_trips_admin_secret() {
let canonical = canonicalize_basic_credentials(b"Basic YWRtaW46c2VjcmV0")
.expect("well-formed credential should canonicalize");
assert_eq!(canonical, format!("admin:{SECRET_SHA256_HEX}"));
}
#[test]
fn canonicalize_is_case_insensitive_on_scheme() {
let canonical = canonicalize_basic_credentials(b"basic YWRtaW46c2VjcmV0")
.expect("lowercase scheme should still canonicalize");
assert_eq!(canonical, format!("admin:{SECRET_SHA256_HEX}"));
}
#[test]
fn canonicalize_rejects_non_basic_scheme() {
assert!(canonicalize_basic_credentials(b"Bearer token").is_none());
}
#[test]
fn canonicalize_rejects_garbage_base64() {
assert!(canonicalize_basic_credentials(b"Basic !!not-base64!!").is_none());
}
#[test]
fn canonicalize_rejects_missing_colon() {
assert!(canonicalize_basic_credentials(b"Basic YWRtaW4=").is_none());
}
#[test]
fn canonicalize_rejects_oversized_payload() {
let payload = "a".repeat(max_decoded_credential_bytes() * 2);
let token = STANDARD.encode(format!("{payload}:pwd"));
let header = format!("Basic {token}");
assert!(canonicalize_basic_credentials(header.as_bytes()).is_none());
}
#[test]
fn canonicalize_rejects_oversized_encoded_payload_before_decode() {
let oversize = "A".repeat(max_decoded_credential_bytes() * 16);
let header = format!("Basic {oversize}");
assert!(canonicalize_basic_credentials(header.as_bytes()).is_none());
}
#[test]
fn set_max_decoded_credential_bytes_zero_is_noop() {
let before = max_decoded_credential_bytes();
set_max_decoded_credential_bytes(0);
assert_eq!(max_decoded_credential_bytes(), before);
}
#[test]
fn check_authorized_hashes_full_pass_match() {
let valid = format!("admin:{SECRET_SHA256_HEX}");
let other = "user:0000000000000000000000000000000000000000000000000000000000000000";
let list = [other.to_owned(), valid.clone()];
assert!(check_authorized_hashes(&valid, &list));
}
#[test]
fn check_authorized_hashes_rejects_wrong_password() {
let wrong = "admin:0000000000000000000000000000000000000000000000000000000000000000";
let list = [format!("admin:{SECRET_SHA256_HEX}")];
assert!(!check_authorized_hashes(wrong, &list));
}
#[test]
fn check_authorized_hashes_rejects_overlong_candidate() {
let long_user = "a".repeat(250);
let stored = format!("{long_user}:{SECRET_SHA256_HEX}");
let attacker =
format!("{long_user}:0000000000000000000000000000000000000000000000000000000000000000");
assert!(stored.len() > AUTH_COMPARE_PAD_LEN);
assert!(attacker.len() > AUTH_COMPARE_PAD_LEN);
assert_eq!(stored.len(), attacker.len()); let list = [stored];
assert!(!check_authorized_hashes(&attacker, &list));
}
#[test]
fn check_authorized_hashes_skips_overlong_stored_entry() {
let valid = format!("admin:{SECRET_SHA256_HEX}");
let overlong = format!("{}:{SECRET_SHA256_HEX}", "a".repeat(250));
assert!(overlong.len() > AUTH_COMPARE_PAD_LEN);
let list = [overlong, valid.clone()];
assert!(check_authorized_hashes(&valid, &list));
}
#[test]
fn check_authorized_hashes_rejects_when_list_empty() {
let candidate = format!("admin:{SECRET_SHA256_HEX}");
assert!(!check_authorized_hashes(&candidate, &[]));
}
}