1pub const NOTES_MAX: usize = 64 * 1024;
19
20const PREFIXES: [(&str, usize); 11] = [
23 ("sk-ant-", 16),
24 ("sk-", 24),
25 ("ghp_", 20),
26 ("gho_", 20),
27 ("ghu_", 20),
28 ("ghs_", 20),
29 ("ghr_", 20),
30 ("github_pat_", 24),
31 ("glpat-", 20),
32 ("xox", 20),
33 ("AKIA", 20),
34];
35
36const RUN: usize = 32;
39
40pub fn check(text: &str) -> Result<(), String> {
57 if text.len() > NOTES_MAX {
58 return Err(format!(
59 "the notes have {} bytes, and the limit is {NOTES_MAX} bytes (64 KiB). Make the text shorter.",
60 text.len()
61 ));
62 }
63 if looks_like_secret(text) {
64 return Err(
65 "the text looks like it has a secret (a token or a key). Take it out: riff never \
66 keeps a secret in the notes."
67 .to_owned(),
68 );
69 }
70 Ok(())
71}
72
73fn looks_like_secret(text: &str) -> bool {
74 if text.contains("PRIVATE KEY-----") {
75 return true;
76 }
77 let words = text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '-' || c == '_'));
78 let prefixed = words.clone().any(|word| {
79 PREFIXES
80 .iter()
81 .any(|(prefix, least)| word.starts_with(prefix) && word.len() >= *least)
82 });
83 prefixed
84 || text
85 .split(|c: char| !c.is_ascii_alphanumeric())
86 .any(is_run_of_a_token)
87}
88
89fn is_run_of_a_token(run: &str) -> bool {
90 run.len() >= RUN
91 && run.bytes().any(|b| b.is_ascii_digit())
92 && run.bytes().any(|b| b.is_ascii_alphabetic())
93 && !run.bytes().all(|b| b.is_ascii_hexdigit())
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn the_limit_is_in_bytes_and_a_text_at_the_limit_is_kept() {
102 assert!(check(&"a".repeat(NOTES_MAX)).is_ok());
103 let over = check(&"a".repeat(NOTES_MAX + 1)).unwrap_err();
104 assert!(over.contains("65536"), "{over}");
105 }
106
107 #[test]
108 fn a_refusal_does_not_repeat_the_secret() {
109 let secret = "ghp_abcdefghijklmnopqrstuvwxyz0123456789";
110 let refused = check(&format!("token {secret}")).unwrap_err();
111 assert!(!refused.contains("ghp_"), "{refused}");
112 }
113
114 #[test]
115 fn each_prefix_of_a_token_is_found() {
116 for token in [
117 "sk-ant-oat01-abcdefghijkl",
118 "github_pat_11ABCDEFG0abcdefghijkl",
119 "glpat-abcdefghijklmnopqrstu",
120 "xoxb-123456789012-abcdefgh",
121 "AKIAIOSFODNN7EXAMPLE",
122 ] {
123 assert!(check(&format!("x {token} y")).is_err(), "{token}");
124 }
125 assert!(check("the sk-learn skill and the ghp_ prefix").is_ok());
126 }
127}