-
Notifications
You must be signed in to change notification settings - Fork 79
feat(asb): authenticate the rpc server #1095
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -157,6 +157,7 @@ pub async fn main() -> Result<()> { | |
| resume_only, | ||
| rpc_bind_host, | ||
| rpc_bind_port, | ||
| rpc_auth_file, | ||
| } => { | ||
| let db = open_db(db_file, AccessMode::ReadWrite, None).await?; | ||
|
|
||
|
|
@@ -388,9 +389,26 @@ pub async fn main() -> Result<()> { | |
|
|
||
| // Start RPC server conditionally | ||
| let _rpc_server = if let (Some(host), Some(port)) = (rpc_bind_host, rpc_bind_port) { | ||
| let auth_file = rpc_auth_file.context( | ||
| "The JSON-RPC server requires authentication: pass --rpc-auth-file pointing at a `salt:hmac` verifier file", | ||
| )?; | ||
| let auth_verifier = std::fs::read_to_string(&auth_file) | ||
|
Author
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. extact this into a function (together with the sanity check below) |
||
| .with_context(|| { | ||
| format!("Failed to read RPC auth file at {}", auth_file.display()) | ||
| })? | ||
| .trim() | ||
| .to_string(); | ||
| if !auth_verifier.contains(':') { | ||
| anyhow::bail!( | ||
| "RPC auth file at {} is malformed: expected `salt:hmac`", | ||
| auth_file.display() | ||
| ); | ||
| } | ||
|
|
||
| let rpc_server = RpcServer::start( | ||
| host, | ||
| port, | ||
| Some(auth_verifier), | ||
| bitcoin_wallet.clone(), | ||
| monero_wallet.clone(), | ||
| event_loop_service, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,3 +2,4 @@ pub mod config; | |
| pub mod defaults; | ||
| pub mod env; | ||
| pub mod prompt; | ||
| pub mod rpc_auth; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| use hmac::{Hmac, Mac}; | ||
| use rand::RngCore; | ||
| use sha2::Sha256; | ||
|
|
||
| type HmacSha256 = Hmac<Sha256>; | ||
|
|
||
| const SALT_BYTES: usize = 16; | ||
| const MIN_PASSWORD_LENGTH: usize = 16; | ||
|
|
||
| /// Builds a `<salt>:<hmac>` verifier for the password using a fresh random salt. | ||
| pub fn generate(password: &str) -> String { | ||
| let mut salt = [0u8; SALT_BYTES]; | ||
| rand::thread_rng().fill_bytes(&mut salt); | ||
| let salt = hex::encode(salt); | ||
| let hmac = hash_with_salt(password, &salt); | ||
| format!("{salt}:{hmac}") | ||
| } | ||
|
|
||
| /// Constant-time check of a password against a `<salt>:<hmac>` verifier. | ||
|
Author
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. dont say its constant time |
||
| /// A malformed verifier never authenticates. | ||
| pub fn verify(password: &str, verifier: &str) -> bool { | ||
| let Some((salt, expected)) = verifier.split_once(':') else { | ||
| return false; | ||
| }; | ||
| let Ok(expected) = hex::decode(expected) else { | ||
| return false; | ||
| }; | ||
|
|
||
| let mut mac = HmacSha256::new_from_slice(salt.as_bytes()).expect("HMAC accepts any key length"); | ||
| mac.update(password.as_bytes()); | ||
| mac.verify_slice(&expected).is_ok() | ||
| } | ||
|
|
||
| fn hash_with_salt(password: &str, salt: &str) -> String { | ||
| let mut mac = HmacSha256::new_from_slice(salt.as_bytes()).expect("HMAC accepts any key length"); | ||
| mac.update(password.as_bytes()); | ||
| hex::encode(mac.finalize().into_bytes()) | ||
| } | ||
|
|
||
| /// Rejects weak passwords. Requires length and a mix of character classes, | ||
| /// and forbids whitespace to keep the value unambiguous in an HTTP header. | ||
| pub fn validate_password_strength(password: &str) -> Result<(), String> { | ||
| let mut missing = Vec::new(); | ||
|
|
||
| if password.chars().count() < MIN_PASSWORD_LENGTH { | ||
| missing.push(format!("at least {MIN_PASSWORD_LENGTH} characters")); | ||
| } | ||
| if !password.chars().any(|c| c.is_ascii_lowercase()) { | ||
| missing.push("a lowercase letter".to_string()); | ||
| } | ||
| if !password.chars().any(|c| c.is_ascii_uppercase()) { | ||
| missing.push("an uppercase letter".to_string()); | ||
| } | ||
| if !password.chars().any(|c| c.is_ascii_digit()) { | ||
| missing.push("a digit".to_string()); | ||
| } | ||
| if !password | ||
| .chars() | ||
| .any(|c| !c.is_ascii_alphanumeric() && !c.is_whitespace()) | ||
| { | ||
| missing.push("a special character".to_string()); | ||
| } | ||
|
|
||
| if password.chars().any(char::is_whitespace) { | ||
| return Err("Password must not contain whitespace".to_string()); | ||
| } | ||
| if missing.is_empty() { | ||
| return Ok(()); | ||
| } | ||
|
|
||
| Err(format!("Password is too weak; it must have {}", missing.join(", "))) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn generate_then_verify_roundtrips() { | ||
| let verifier = generate("Str0ng!Passphrase42xx"); | ||
| assert!(verify("Str0ng!Passphrase42xx", &verifier)); | ||
| assert!(!verify("not the password", &verifier)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn verify_is_reproducible_for_a_fixed_salt() { | ||
| let verifier = format!("deadbeef:{}", hash_with_salt("hunter2", "deadbeef")); | ||
| assert!(verify("hunter2", &verifier)); | ||
| assert!(!verify("hunter3", &verifier)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn malformed_verifiers_never_authenticate() { | ||
| assert!(!verify("pw", "missing-colon")); | ||
| assert!(!verify("pw", "salt:not-hex")); | ||
| assert!(!verify("pw", "")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn strength_rejects_weak_and_accepts_strong() { | ||
| assert!(validate_password_strength("Sh0rt!").is_err()); | ||
| assert!(validate_password_strength("alllowercaseletters").is_err()); | ||
| assert!(validate_password_strength("has a space In It 9!").is_err()); | ||
| assert!(validate_password_strength("Str0ng!Passphrase42xx").is_ok()); | ||
| } | ||
| } | ||
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.
salt:hmac is too specific here