feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305 encrypted secret storage, QR code generation, and bcrypt-hashed backup codes - API key switcher: OAuth vs personal API key toggle in AIUI chat settings with status indicator, key validation, and help text - Login progress bar: server startup detection with health check polling, form disabled until server is ready - AI quarantine docs: comprehensive HTML page documenting all 6 security layers - Settings: AI Data Access permission toggles with per-category control - Alpha hardening plan: 28-task overnight automation plan across 7 phases (onboarding, login, app install, AIUI, UI polish, security, ISO build) - Backlog: node discovery spatial map feature for alpha demo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs;
|
||||
|
||||
use crate::totp::TotpData;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct OnboardingState {
|
||||
complete: bool,
|
||||
@@ -17,6 +19,8 @@ pub struct User {
|
||||
pub password_hash: String,
|
||||
pub setup_complete: bool,
|
||||
pub onboarding_complete: bool,
|
||||
#[serde(default)]
|
||||
pub totp: Option<TotpData>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -58,6 +62,7 @@ impl AuthManager {
|
||||
password_hash,
|
||||
setup_complete: true,
|
||||
onboarding_complete,
|
||||
totp: None,
|
||||
};
|
||||
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
@@ -123,6 +128,39 @@ impl AuthManager {
|
||||
.unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Check if 2FA is enabled for the user.
|
||||
pub async fn is_totp_enabled(&self) -> Result<bool> {
|
||||
Ok(self.get_user().await?.map(|u| u.totp.is_some()).unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Get the TOTP data (if 2FA is enabled).
|
||||
pub async fn get_totp_data(&self) -> Result<Option<TotpData>> {
|
||||
Ok(self.get_user().await?.and_then(|u| u.totp))
|
||||
}
|
||||
|
||||
/// Save TOTP data to user.json (enable 2FA).
|
||||
pub async fn save_totp(&self, totp_data: TotpData) -> Result<()> {
|
||||
let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
|
||||
user.totp = Some(totp_data);
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove TOTP data from user.json (disable 2FA).
|
||||
pub async fn remove_totp(&self) -> Result<()> {
|
||||
let mut user = self.get_user().await?.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
|
||||
user.totp = None;
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
fs::write(&user_file, serde_json::to_string_pretty(&user)?).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update TOTP data in place (e.g. after consuming a backup code or recording a used step).
|
||||
pub async fn update_totp(&self, totp_data: TotpData) -> Result<()> {
|
||||
self.save_totp(totp_data).await
|
||||
}
|
||||
|
||||
pub async fn verify_password(&self, password: &str) -> Result<bool> {
|
||||
use bcrypt::verify;
|
||||
|
||||
@@ -157,6 +195,13 @@ impl AuthManager {
|
||||
.ok_or_else(|| anyhow::anyhow!("User not set up"))?;
|
||||
|
||||
user.password_hash = password_hash;
|
||||
|
||||
// Re-encrypt TOTP MEK under new password if 2FA is enabled
|
||||
if let Some(ref totp_data) = user.totp {
|
||||
let rekeyed = crate::totp::rekey(totp_data, current_password, new_password)?;
|
||||
user.totp = Some(rekeyed);
|
||||
}
|
||||
|
||||
let user_file = self.data_dir.join("user.json");
|
||||
let content = serde_json::to_string_pretty(&user)?;
|
||||
fs::write(&user_file, content).await?;
|
||||
|
||||
Reference in New Issue
Block a user