Skip to main content

zeph_common/
secrets.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Canonical secret-token and path prefixes shared by redaction layers across crates.
5//!
6//! `zeph-core::redact` and `zeph-memory::store::compression_guidelines` both scrub
7//! secrets and filesystem paths before persisting or displaying untrusted text. Each
8//! crate previously carried its own hand-rolled copy of these lists, and the copies had
9//! already begun to drift from each other (see #5917). This module is the single source
10//! of truth for the raw prefixes/patterns; consumers compile their own `regex::Regex`
11//! instances from these constants — `zeph-common` does not depend on `regex` outside of
12//! tests, matching the pattern established by [`crate::patterns`].
13
14/// Prefixes of API keys, tokens, and other secret material recognized across Zeph.
15///
16/// Each entry is a literal prefix, not regex-escaped. Consumers building a regex
17/// alternation from this list must escape entries themselves (e.g. via `regex::escape`),
18/// since `ya29.` contains a literal `.` that must not be treated as "match any character".
19pub const SECRET_PREFIXES: &[&str] = &[
20    "sk-",
21    "sk_live_",
22    "sk_test_",
23    "AKIA",
24    "ghp_",
25    "gho_",
26    "-----BEGIN",
27    "xoxb-",
28    "xoxp-",
29    "AIza",
30    "ya29.",
31    "glpat-",
32    "hf_",
33    "npm_",
34    "dckr_pat_",
35];
36
37/// Absolute filesystem path prefixes redacted before persisting or displaying untrusted
38/// text, to avoid leaking local usernames or directory layout.
39pub const PATH_PREFIXES: &[&str] = &["/home/", "/Users/", "/root/", "/tmp/", "/var/"];
40
41/// Regex pattern matching `Authorization: Bearer <token>` headers.
42///
43/// Capture group 1 covers the header name up to and including the token's leading
44/// whitespace, so replacing with `"${1}[REDACTED]"` preserves the header name while
45/// redacting only the token value.
46pub const BEARER_TOKEN_PATTERN: &str = r"(?i)(Authorization:\s*Bearer\s+)\S+";
47
48/// Regex pattern matching standalone JWTs (three Base64url-encoded segments separated by
49/// dots).
50///
51/// The final segment uses `*` (not `+`) so it also matches `alg=none` JWTs, which carry an
52/// empty signature segment.
53pub const JWT_PATTERN: &str = r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*";
54
55#[cfg(test)]
56mod tests {
57    use regex::Regex;
58
59    use super::*;
60
61    #[test]
62    fn bearer_pattern_compiles_and_matches() {
63        let re = Regex::new(BEARER_TOKEN_PATTERN).unwrap();
64        assert!(re.is_match("Authorization: Bearer abc.def.ghi"));
65    }
66
67    #[test]
68    fn jwt_pattern_compiles_and_matches() {
69        let re = Regex::new(JWT_PATTERN).unwrap();
70        assert!(re.is_match("eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.sig"));
71    }
72
73    #[test]
74    fn jwt_pattern_matches_alg_none_empty_signature() {
75        let re = Regex::new(JWT_PATTERN).unwrap();
76        assert!(re.is_match("eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1c2VyIn0."));
77    }
78
79    #[test]
80    fn secret_prefixes_build_valid_escaped_regex_alternation() {
81        let pattern = SECRET_PREFIXES
82            .iter()
83            .map(|p| regex::escape(p))
84            .collect::<Vec<_>>()
85            .join("|");
86        let full = format!("(?:{pattern})[^\\s]*");
87        let re = Regex::new(&full).expect("alternation built from SECRET_PREFIXES must compile");
88        assert!(re.is_match("sk-abc123"));
89        assert!(re.is_match("ya29.a0AfH6"));
90    }
91
92    #[test]
93    fn path_prefixes_build_valid_regex_alternation() {
94        let pattern = PATH_PREFIXES.join("|");
95        let full = format!("(?:{pattern})[^\\s]*");
96        let re = Regex::new(&full).expect("alternation built from PATH_PREFIXES must compile");
97        assert!(re.is_match("/home/user/file"));
98    }
99}