1use base64::Engine;
2use rand::RngCore;
3use serde::Serialize;
4
5const DEFAULT_SECRET: &str = "openauth-secret-123456789012345678901";
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
8#[serde(rename_all = "lowercase")]
9pub enum SecretSeverity {
10 Ok,
11 Warning,
12 Error,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct SecretAssessment {
17 pub severity: SecretSeverity,
18 pub message: String,
19}
20
21pub fn generate_secret(bytes: usize) -> String {
22 let mut buffer = vec![0_u8; bytes.max(32)];
23 rand::rngs::OsRng.fill_bytes(&mut buffer);
24 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buffer)
25}
26
27pub fn assess_secret(secret: &str, production: bool) -> SecretAssessment {
28 if secret.is_empty() {
29 return error("OpenAuth secret is missing.");
30 }
31 if production && secret == DEFAULT_SECRET {
32 return error("The default OpenAuth secret cannot be used in production.");
33 }
34 if looks_like_example_secret(secret) {
35 return error("The configured secret looks like an example value.");
36 }
37 if secret.len() < 32 {
38 return error("Secret is too short; use at least 32 bytes of random material.");
39 }
40 if character_classes(secret) < 3 {
41 return error("Secret has low character diversity; generate a random secret.");
42 }
43 if repeated_single_character(secret) {
44 return error("Secret has low entropy; generate a random secret.");
45 }
46
47 SecretAssessment {
48 severity: SecretSeverity::Ok,
49 message: "Secret strength looks good.".to_owned(),
50 }
51}
52
53fn looks_like_example_secret(secret: &str) -> bool {
54 let lower = secret.to_ascii_lowercase();
55 lower.contains("secret-a-at-least-32-chars")
56 || lower.contains("change-me")
57 || lower.contains("example")
58 || lower.contains("your-secret")
59 || lower.contains("openauth-example")
60}
61
62fn character_classes(secret: &str) -> usize {
63 [
64 secret
65 .chars()
66 .any(|character| character.is_ascii_lowercase()),
67 secret
68 .chars()
69 .any(|character| character.is_ascii_uppercase()),
70 secret.chars().any(|character| character.is_ascii_digit()),
71 secret
72 .chars()
73 .any(|character| !character.is_ascii_alphanumeric()),
74 ]
75 .into_iter()
76 .filter(|present| *present)
77 .count()
78}
79
80fn repeated_single_character(secret: &str) -> bool {
81 let mut chars = secret.chars();
82 let Some(first) = chars.next() else {
83 return true;
84 };
85 chars.all(|character| character == first)
86}
87
88fn error(message: &str) -> SecretAssessment {
89 SecretAssessment {
90 severity: SecretSeverity::Error,
91 message: message.to_owned(),
92 }
93}