tsafe-attest 1.1.0

Attestation pipeline for tsafe — secret scanner + env-injection contract + run-evidence harness (algol-merged)
Documentation
//! Value redaction + placeholder detection.
//!
//! Ported from `algol/src/redact.rs`. Phase 3 swaps the canonical hash
//! function from SHA-256 to BLAKE3 per ec ADR-0003; SHA-256 remains
//! available via the deprecation API in [`crate::hash::sha256_hash`].

use crate::hash::blake3_hash;

/// Compute the BLAKE3 content hash of a value, returning `blake3:<hex>`.
///
/// **Phase 3 wire-format change**: this is BLAKE3 (was SHA-256 in Phase 2
/// algol). Consumers pinning the prefix as a content-address must update;
/// see CHANGELOG.
pub fn fingerprint(value: impl AsRef<[u8]>) -> String {
    blake3_hash(value)
}

/// Produce a redacted representation of `value` for human display.
///
/// Short values (≤ 8 chars) collapse to `****`. Longer values keep the
/// first and last 4 chars (`abcd****wxyz`) so the operator can recognise
/// the slot without leaking the body.
pub fn redacted(value: &str) -> String {
    if value.chars().count() <= 8 {
        return "****".to_string();
    }
    let prefix: String = value.chars().take(4).collect();
    let suffix: String = value
        .chars()
        .rev()
        .take(4)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect();
    format!("{prefix}****{suffix}")
}

/// Truncate a content-hash for display: keep the prefix tag + first 8 hex
/// chars (`blake3:01234567...`).
pub fn short_hash(hash: &str) -> String {
    // Accept either `blake3:` (Phase 3+) or `sha256:` (Phase 1-2 compat).
    for prefix in ["blake3:", "sha256:"] {
        if let Some(hex) = hash.strip_prefix(prefix) {
            let head = &hex[..hex.len().min(8)];
            return format!("{prefix}{head}...");
        }
    }
    hash.to_string()
}

/// Detect "placeholder" values that should reduce finding confidence.
///
/// Common patterns: empty, `xxxxxxxx`, `changeme`, `your-key-here`,
/// `example`, `dummy`, `fake`, `test`. Used by the scanner to soften
/// confidence on .env-style entries that obviously aren't real secrets.
pub fn is_placeholder(value: &str) -> bool {
    let lowered = value.trim_matches(['"', '\'']).to_ascii_lowercase();
    lowered.is_empty()
        || lowered.contains("changeme")
        || lowered.contains("example")
        || lowered.contains("dummy")
        || lowered.contains("fake")
        || lowered.contains("test")
        || lowered.contains("your-key-here")
        || lowered == "xxx"
        || lowered == "xxxx"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn redacts_short_values_fully() {
        assert_eq!(redacted("short"), "****");
    }

    #[test]
    fn redacts_long_values_with_prefix_and_suffix() {
        assert_eq!(redacted("long_demo_token_1234567890"), "long****7890");
    }

    #[test]
    fn fingerprint_uses_blake3_prefix() {
        let h = fingerprint("payload");
        assert!(
            h.starts_with("blake3:"),
            "Phase 3 fingerprint must be BLAKE3 per ec ADR-0003, got {h:?}"
        );
    }

    #[test]
    fn fingerprint_is_deterministic() {
        assert_eq!(fingerprint("same"), fingerprint("same"));
        assert_ne!(fingerprint("same"), fingerprint("different"));
    }

    #[test]
    fn short_hash_handles_blake3_prefix() {
        let h = fingerprint("abc");
        let short = short_hash(&h);
        assert!(short.starts_with("blake3:"));
        assert!(short.ends_with("..."));
        assert!(short.len() < h.len());
    }

    #[test]
    fn short_hash_accepts_legacy_sha256_prefix() {
        let h = format!("sha256:{}", "a".repeat(64));
        let short = short_hash(&h);
        assert!(short.starts_with("sha256:"));
        assert!(short.ends_with("..."));
    }

    #[test]
    fn redaction_does_not_leak_whole_value() {
        let secret = "tsafe_demo_token_1234567890";
        assert!(!redacted(secret).contains(secret));
    }
}