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}