Skip to main content

openauth_cli/
secret.rs

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}