security: migrate bcrypt→Argon2id, random Bitcoin RPC password
Password hashing migrated from bcrypt to Argon2id (m=64MiB, t=3, p=4). Transparent upgrade: on successful bcrypt login, re-hashes with Argon2id and persists. New signups and password changes use Argon2id directly. Unifies crypto stack — Argon2id was already used for TOTP and backup KDF. Bitcoin RPC password: no longer falls back to hardcoded "archipelago123". On first boot, generates a random 32-char hex password from CSPRNG, saves to /var/lib/archipelago/secrets/bitcoin-rpc-password with 0600 permissions. Existing installs with secrets file are unaffected. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7bbd8f889a
commit
adcc3fddc7
@ -110,9 +110,7 @@ impl AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn setup_user(&self, password: &str) -> Result<()> {
|
pub async fn setup_user(&self, password: &str) -> Result<()> {
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
let password_hash = argon2id_hash(password)?;
|
||||||
|
|
||||||
let password_hash = hash(password, DEFAULT_COST)?;
|
|
||||||
|
|
||||||
// If onboarding was already completed (before setup), preserve that
|
// If onboarding was already completed (before setup), preserve that
|
||||||
let onboarding_complete = self.is_onboarding_complete().await?;
|
let onboarding_complete = self.is_onboarding_complete().await?;
|
||||||
@ -222,10 +220,25 @@ impl AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn verify_password(&self, password: &str) -> Result<bool> {
|
pub async fn verify_password(&self, password: &str) -> Result<bool> {
|
||||||
use bcrypt::verify;
|
|
||||||
|
|
||||||
if let Some(user) = self.get_user().await? {
|
if let Some(user) = self.get_user().await? {
|
||||||
Ok(verify(password, &user.password_hash)?)
|
// Detect hash format and verify accordingly
|
||||||
|
if user.password_hash.starts_with("$2") {
|
||||||
|
// Legacy bcrypt hash — verify then auto-upgrade to Argon2id
|
||||||
|
let valid = bcrypt::verify(password, &user.password_hash)?;
|
||||||
|
if valid {
|
||||||
|
// Transparent upgrade: re-hash with Argon2id on successful login
|
||||||
|
let new_hash = argon2id_hash(password)?;
|
||||||
|
let mut upgraded = user.clone();
|
||||||
|
upgraded.password_hash = new_hash;
|
||||||
|
let user_file = self.data_dir.join("user.json");
|
||||||
|
fs::write(&user_file, serde_json::to_string_pretty(&upgraded)?).await?;
|
||||||
|
tracing::info!("Upgraded password hash from bcrypt to Argon2id");
|
||||||
|
}
|
||||||
|
Ok(valid)
|
||||||
|
} else {
|
||||||
|
// Argon2id hash (PHC string format: $argon2id$...)
|
||||||
|
Ok(argon2id_verify(password, &user.password_hash))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
@ -239,15 +252,13 @@ impl AuthManager {
|
|||||||
new_password: &str,
|
new_password: &str,
|
||||||
also_change_ssh: bool,
|
also_change_ssh: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
|
||||||
|
|
||||||
if !self.verify_password(current_password).await? {
|
if !self.verify_password(current_password).await? {
|
||||||
anyhow::bail!("Current password is incorrect");
|
anyhow::bail!("Current password is incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_password_strength(new_password)?;
|
validate_password_strength(new_password)?;
|
||||||
|
|
||||||
let password_hash = hash(new_password, DEFAULT_COST)?;
|
let password_hash = argon2id_hash(new_password)?;
|
||||||
|
|
||||||
let mut user = self
|
let mut user = self
|
||||||
.get_user()
|
.get_user()
|
||||||
@ -427,3 +438,32 @@ async fn change_ssh_password(new_password: &str) -> Result<()> {
|
|||||||
tracing::info!("SSH password updated for user {}", ssh_user);
|
tracing::info!("SSH password updated for user {}", ssh_user);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hash a password with Argon2id (memory-hard, GPU/ASIC resistant).
|
||||||
|
/// Uses PHC string format ($argon2id$v=19$m=65536,t=3,p=4$...) for self-describing storage.
|
||||||
|
fn argon2id_hash(password: &str) -> Result<String> {
|
||||||
|
use argon2::{Argon2, Params, PasswordHasher};
|
||||||
|
use argon2::password_hash::SaltString;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let params = Params::new(65536, 3, 4, Some(32))
|
||||||
|
.map_err(|e| anyhow::anyhow!("Invalid Argon2 params: {}", e))?;
|
||||||
|
let hasher = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
|
||||||
|
let hash = hasher
|
||||||
|
.hash_password(password.as_bytes(), &salt)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Argon2id hash failed: {}", e))?;
|
||||||
|
Ok(hash.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a password against an Argon2id PHC string hash.
|
||||||
|
fn argon2id_verify(password: &str, hash: &str) -> bool {
|
||||||
|
use argon2::{Argon2, PasswordVerifier};
|
||||||
|
use argon2::password_hash::PasswordHash;
|
||||||
|
|
||||||
|
let parsed = match PasswordHash::new(hash) {
|
||||||
|
Ok(h) => h,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
Argon2::default().verify_password(password.as_bytes(), &parsed).is_ok()
|
||||||
|
}
|
||||||
|
|||||||
@ -29,9 +29,32 @@ async fn read_password() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Dev fallback (will only work on dev machines with default config)
|
// 3. Generate a random password and persist it (first-boot provisioning)
|
||||||
debug!("Bitcoin RPC password: using dev fallback");
|
let random_pass = generate_random_password();
|
||||||
"archipelago123".to_string()
|
if let Some(parent) = std::path::Path::new(SECRETS_PATH).parent() {
|
||||||
|
let _ = tokio::fs::create_dir_all(parent).await;
|
||||||
|
}
|
||||||
|
match tokio::fs::write(SECRETS_PATH, &random_pass).await {
|
||||||
|
Ok(_) => {
|
||||||
|
// Restrict permissions to owner-only
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let _ = std::fs::set_permissions(SECRETS_PATH, std::fs::Permissions::from_mode(0o600));
|
||||||
|
}
|
||||||
|
debug!("Bitcoin RPC password: generated and saved to secrets file");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to save generated Bitcoin RPC password: {} — using ephemeral", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
random_pass
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate a cryptographically random password for Bitcoin RPC (32 hex chars).
|
||||||
|
fn generate_random_password() -> String {
|
||||||
|
let bytes: [u8; 16] = rand::random();
|
||||||
|
hex::encode(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get Bitcoin RPC credentials (user, password). Cached after first call.
|
/// Get Bitcoin RPC credentials (user, password). Cached after first call.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user