-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/mail accessibility #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| //! First-boot bootstrap: when no API key is configured, generate an ephemeral one | ||
| //! and print the env line the operator needs to copy back into their environment. | ||
|
|
||
| use rand_core::{OsRng, RngCore}; | ||
|
|
||
| use crate::api_key::{hash_api_key, ApiKeyError}; | ||
|
|
||
| /// Result of a bootstrap run. | ||
| pub struct BootstrapKey { | ||
| pub id: String, | ||
| pub plaintext: String, | ||
| pub hash: String, | ||
| } | ||
|
|
||
| /// Generate a random url-safe plaintext key and return it alongside its argon2 hash. | ||
| pub fn generate_bootstrap_key(id: impl Into<String>) -> Result<BootstrapKey, ApiKeyError> { | ||
| let plaintext = random_token(32); | ||
| let hash = hash_api_key(&plaintext)?; | ||
| Ok(BootstrapKey { | ||
| id: id.into(), | ||
| plaintext, | ||
| hash, | ||
| }) | ||
| } | ||
|
|
||
| /// Generate a random JWT secret (used when the default placeholder is still in place). | ||
| pub fn generate_jwt_secret() -> String { | ||
| random_token(48) | ||
| } | ||
|
|
||
| fn random_token(byte_len: usize) -> String { | ||
| const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||
| let mut bytes = vec![0u8; byte_len]; | ||
| OsRng.fill_bytes(&mut bytes); | ||
| bytes | ||
| .into_iter() | ||
| .map(|b| ALPHABET[(b as usize) % ALPHABET.len()] as char) | ||
| .collect() | ||
|
Comment on lines
+31
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚨 suggestion (security): The mapping Suggested implementation: fn random_token(len: usize) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
// Rejection sampling to avoid modulo bias:
// Draw from a large uniform range and only keep values that fall within
// a multiple of ALPHABET.len(), then map with `% ALPHABET.len()`.
let mut rng = OsRng;
let alphabet_len = ALPHABET.len() as u32;
let zone = (u32::MAX / alphabet_len) * alphabet_len;
let mut out = String::with_capacity(len);
while out.len() < len {
let v = rng.next_u32();
if v < zone {
let idx = (v % alphabet_len) as usize;
out.push(ALPHABET[idx] as char);
}
}
out
}None required, assuming |
||
| } | ||
|
|
||
| /// Print a human-readable banner to stderr so it survives JSON log mode. | ||
| pub fn print_bootstrap_banner(key: &BootstrapKey, jwt_secret_generated: Option<&str>) { | ||
| let mut lines = vec![ | ||
| "".to_string(), | ||
| "===================== MAILIFY BOOTSTRAP =====================".to_string(), | ||
| "No API key was configured. An ephemeral key has been generated".to_string(), | ||
| "for this session. It will NOT survive a restart unless you add".to_string(), | ||
| "the hash below to your environment (e.g. .env / compose env).".to_string(), | ||
| "".to_string(), | ||
| format!(" Key id: {}", key.id), | ||
| format!(" Plaintext: {}", key.plaintext), | ||
| "".to_string(), | ||
| " Paste into your env and restart to persist:".to_string(), | ||
| format!(" MAILIFY_AUTH__API_KEYS__{}={}", key.id, key.hash), | ||
| ]; | ||
| if let Some(secret) = jwt_secret_generated { | ||
| lines.push("".to_string()); | ||
| lines | ||
| .push(" JWT secret was the default placeholder — generated a random one.".to_string()); | ||
| lines.push(" Persist it too (otherwise issued tokens die on restart):".to_string()); | ||
| lines.push(format!(" MAILIFY_AUTH__JWT_SECRET={secret}")); | ||
| } | ||
| lines.push("=============================================================".to_string()); | ||
| lines.push("".to_string()); | ||
| eprintln!("{}", lines.join("\n")); | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::api_key::verify_api_key; | ||
|
|
||
| #[test] | ||
| fn bootstrap_key_verifies_against_its_hash() { | ||
| let key = generate_bootstrap_key("DEFAULT").unwrap(); | ||
| assert_eq!(key.id, "DEFAULT"); | ||
| assert!(verify_api_key(&key.plaintext, &key.hash).unwrap()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn random_tokens_are_unique_and_sized() { | ||
| let a = random_token(32); | ||
| let b = random_token(32); | ||
| assert_eq!(a.len(), 32); | ||
| assert_ne!(a, b); | ||
| } | ||
|
|
||
| #[test] | ||
| fn jwt_secret_is_non_empty() { | ||
| let s = generate_jwt_secret(); | ||
| assert_eq!(s.len(), 48); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,11 @@ | ||
| pub mod api_key; | ||
| pub mod bootstrap; | ||
| pub mod jwt; | ||
| pub mod middleware; | ||
|
|
||
| pub use api_key::{hash_api_key, verify_api_key, ApiKeyError}; | ||
| pub use bootstrap::{ | ||
| generate_bootstrap_key, generate_jwt_secret, print_bootstrap_banner, BootstrapKey, | ||
| }; | ||
| pub use jwt::{Claims, JwtError, JwtIssuer}; | ||
| pub use middleware::{require_jwt, AuthLayer}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚨 issue (security): Bootstrap auth failure is only logged, leaving the server running without any configured API key.
In
maybe_bootstrap_auth, ifgenerate_bootstrap_keyfails, we log and return but still start with an emptyapi_keysmap even thoughauth.bootstrapis enabled. This undermines the expectation that a secured instance always has at least one key. Instead, treat this as a hard startup failure (e.g., return an error frommain/ abort) when bootstrap is requested but key generation fails, rather than continuing without auth material.