Skip to content

Basic Auth Generator — Rust source

Build HTTP Basic Access Authentication headers from a username and password, or decode an existing Authorization header back to its credentials. UTF-8 safe and fully client-side, with a shareable link.

This is the Rust implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! basic-auth-generator — HTTP Basic-Auth header encode/decode.
//!
//! Language: Rust (edition 2021, standard library only)
//! Source:   CosmoDev polyglot showcase port of the Basic Auth Generator tool,
//!           ported from src/lib/basic-auth.ts (the canonical TypeScript
//!           implementation) and kept in lock-step with cli/basic-auth-generator.
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Design goals:
//!   - Pure + deterministic; never panics (public API returns Strings/Options).
//!   - Functionally equivalent to the TS reference + Go twin: same inputs ->
//!     same outputs, same reject behavior on malformed headers.
//!   - Self-contained: std only (no crates.io dependencies — no `base64` crate).
//!
//! Unicode note: Rust's std has no base64 codec (the `base64` crate provides
//! one, but the brief forbids external deps). The full RFC 4648 §4 standard
//! alphabet encoder/decoder is implemented inline below so the port stays
//! dependency-free while remaining byte-for-byte equivalent to Go's
//! encoding/base64 and the TS b64encode/b64decode helpers (which are UTF-8
//! safe). The encoder operates on the UTF-8 byte representation of the input,
//! matching the TS `TextEncoder().encode(str)` path — so `café:päss` encodes
//! correctly, not just ASCII.

/// RFC 4648 §4 standard base64 alphabet.
const B64_ALPHABET: &[u8; 64] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// Credentials hold a parsed username/password pair. Mirrors the Go twin's
/// `Credentials` struct and the TS `BasicAuthCredentials` interface.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Credentials {
    pub user: String,
    pub pass: String,
}

/// Encode arbitrary bytes into standard base64 (with `=` padding), matching
/// Go's `base64.StdEncoding.EncodeToString` and TS's `b64encode`.
///
/// Processes input in 3-byte groups, emitting 4 characters per group. A
/// trailing 1- or 2-byte group produces 2 or 3 characters respectively, padded
/// with `=` to a multiple of 4.
fn b64_encode(input: &[u8]) -> String {
    let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
    for chunk in input.chunks(3) {
        let b0 = chunk[0] as u32;
        let b1 = *chunk.get(1).unwrap_or(&0) as u32;
        let b2 = *chunk.get(2).unwrap_or(&0) as u32;
        let n = (b0 << 16) | (b1 << 8) | b2;
        let i0 = ((n >> 18) & 0x3F) as usize;
        let i1 = ((n >> 12) & 0x3F) as usize;
        let i2 = ((n >> 6) & 0x3F) as usize;
        let i3 = (n & 0x3F) as usize;

        out.push(B64_ALPHABET[i0] as char);
        out.push(B64_ALPHABET[i1] as char);
        match chunk.len() {
            1 => {
                out.push('=');
                out.push('=');
            }
            2 => {
                out.push(B64_ALPHABET[i2] as char);
                out.push('=');
            }
            _ => {
                out.push(B64_ALPHABET[i2] as char);
                out.push(B64_ALPHABET[i3] as char);
            }
        }
    }
    out
}

/// Decode a standard base64 string into bytes. Returns `None` on any invalid
/// character or malformed padding, matching Go's `base64.StdEncoding.DecodeString`
/// (which returns an error) and the TS `b64decode` (which throws → caller maps
/// to None).
///
/// Whitespace inside the token is ignored, mirroring the TS
/// `b64decode(token.replace(/\s+/g, ''))` helper.
fn b64_decode(input: &str) -> Option<Vec<u8>> {
    // Build the reverse lookup once: -1 marks bytes not in the alphabet.
    let mut table = [-1i8; 256];
    for (i, &b) in B64_ALPHABET.iter().enumerate() {
        table[b as usize] = i as i8;
    }

    // Strip whitespace; standard base64 then requires a length that is a
    // multiple of 4 (Go's StdEncoding enforces this; TS's btoa/atob path does
    // too via the browser).
    let cleaned: Vec<u8> = input
        .bytes()
        .filter(|b| !b.is_ascii_whitespace())
        .collect();
    if cleaned.is_empty() {
        return Some(Vec::new());
    }
    if cleaned.len() % 4 != 0 {
        return None;
    }

    let mut out = Vec::with_capacity(cleaned.len() / 4 * 3);
    for chunk in cleaned.chunks(4) {
        let mut idx = [0u8; 4];
        let mut pads = 0u8;
        for (j, &b) in chunk.iter().enumerate() {
            if b == b'=' {
                idx[j] = 0;
                pads += 1;
            } else {
                let v = table[b as usize];
                if v < 0 {
                    return None; // invalid character
                }
                idx[j] = v as u8;
            }
        }
        let n = ((idx[0] as u32) << 18)
            | ((idx[1] as u32) << 12)
            | ((idx[2] as u32) << 6)
            | (idx[3] as u32);
        out.push((n >> 16) as u8);
        if pads < 2 {
            out.push((n >> 8) as u8);
        }
        if pads < 1 {
            out.push(n as u8);
        }
    }
    Some(out)
}

/// Build an HTTP Basic-Auth header value: `"Basic " + base64(user:pass)`.
/// Mirrors `BuildHeader` in the Go twin and `buildBasicAuth` in the TS source.
pub fn build_header(user: &str, pass: &str) -> String {
    let raw = format!("{}:{}", user, pass);
    format!("Basic {}", b64_encode(raw.as_bytes()))
}

/// Parse a `"Basic <token>"` header back into credentials, returning `None`
/// if the header is malformed or the credentials have no colon separator.
///
/// Logic mirrors `ParseHeader` in the Go twin exactly: trim the header,
/// require a case-insensitive `"basic "` prefix, trim the remaining token,
/// base64-decode it, then split on the FIRST colon (so a password containing
/// colons round-trips).
pub fn parse_header(header: &str) -> Option<Credentials> {
    let h = header.trim();
    let prefix = "basic ";
    if h.len() < prefix.len() || !h.to_ascii_lowercase().starts_with(prefix) {
        return None;
    }
    let token = h[prefix.len()..].trim();
    let bytes = b64_decode(token)?;
    let decoded = String::from_utf8(bytes).ok()?;
    let (user, pass) = decoded.split_once(':')?;
    Some(Credentials {
        user: user.to_string(),
        pass: pass.to_string(),
    })
}

// ---------- tests (showcase-only; the canonical suite lives in src/lib) ----------
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn build_known_vector() {
        assert_eq!(build_header("user", "pass"), "Basic dXNlcjpwYXNz");
    }

    #[test]
    fn round_trip() {
        for (user, pass) in [("alice", "s3cr3t"), ("", "x"), ("x", ""), ("u", "a:b:c")] {
            let creds = parse_header(&build_header(user, pass))
                .expect("round-trip should parse");
            assert_eq!(creds.user, user);
            assert_eq!(creds.pass, pass, "password mismatch for ({user:?},{pass:?})");
        }
    }

    #[test]
    fn password_with_colon_preserved() {
        // Split happens on the FIRST colon only — the rest stays in the password.
        let creds = parse_header(&build_header("u", "a:b:c")).unwrap();
        assert_eq!(creds.user, "u");
        assert_eq!(creds.pass, "a:b:c");
    }

    #[test]
    fn rejects_malformed() {
        for bad in [
            "Bearer xyz",
            "",
            "Basic !!!not-base64!!!",
            "Basic bm9jb2xvbg==", // "nocolon" — no colon separator
        ] {
            assert!(parse_header(bad).is_none(), "{bad:?} should be rejected");
        }
    }
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →