this repo has no description
1use crate::api::EmptyResponse; 2use crate::api::error::ApiError; 3use crate::auth::BearerAuth; 4use crate::auth::totp::{ 5 decrypt_totp_secret, encrypt_totp_secret, generate_backup_codes, generate_qr_png_base64, 6 generate_totp_secret, generate_totp_uri, hash_backup_code, is_backup_code_format, 7 verify_backup_code, verify_totp_code, 8}; 9use crate::state::{AppState, RateLimitKind}; 10use crate::types::PlainPassword; 11use axum::{ 12 Json, 13 extract::State, 14 response::{IntoResponse, Response}, 15}; 16use chrono::Utc; 17use serde::{Deserialize, Serialize}; 18use tracing::{error, info, warn}; 19 20const ENCRYPTION_VERSION: i32 = 1; 21 22#[derive(Serialize)] 23#[serde(rename_all = "camelCase")] 24pub struct CreateTotpSecretResponse { 25 pub secret: String, 26 pub uri: String, 27 pub qr_base64: String, 28} 29 30pub async fn create_totp_secret(State(state): State<AppState>, auth: BearerAuth) -> Response { 31 let existing = sqlx::query_scalar!( 32 "SELECT verified FROM user_totp WHERE did = $1", 33 &*&auth.0.did 34 ) 35 .fetch_optional(&state.db) 36 .await; 37 38 if let Ok(Some(true)) = existing { 39 return ApiError::TotpAlreadyEnabled.into_response(); 40 } 41 42 let secret = generate_totp_secret(); 43 44 let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", &*&auth.0.did) 45 .fetch_optional(&state.db) 46 .await; 47 48 let handle = match handle { 49 Ok(Some(h)) => h, 50 Ok(None) => return ApiError::AccountNotFound.into_response(), 51 Err(e) => { 52 error!("DB error fetching handle: {:?}", e); 53 return ApiError::InternalError(None).into_response(); 54 } 55 }; 56 57 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 58 let uri = generate_totp_uri(&secret, &handle, &hostname); 59 60 let qr_code = match generate_qr_png_base64(&secret, &handle, &hostname) { 61 Ok(qr) => qr, 62 Err(e) => { 63 error!("Failed to generate QR code: {:?}", e); 64 return ApiError::InternalError(Some("Failed to generate QR code".into())) 65 .into_response(); 66 } 67 }; 68 69 let encrypted_secret = match encrypt_totp_secret(&secret) { 70 Ok(enc) => enc, 71 Err(e) => { 72 error!("Failed to encrypt TOTP secret: {:?}", e); 73 return ApiError::InternalError(None).into_response(); 74 } 75 }; 76 77 let result = sqlx::query!( 78 r#" 79 INSERT INTO user_totp (did, secret_encrypted, encryption_version, verified, created_at) 80 VALUES ($1, $2, $3, false, NOW()) 81 ON CONFLICT (did) DO UPDATE SET 82 secret_encrypted = $2, 83 encryption_version = $3, 84 verified = false, 85 created_at = NOW(), 86 last_used = NULL 87 "#, 88 &auth.0.did, 89 encrypted_secret, 90 ENCRYPTION_VERSION 91 ) 92 .execute(&state.db) 93 .await; 94 95 if let Err(e) = result { 96 error!("Failed to store TOTP secret: {:?}", e); 97 return ApiError::InternalError(None).into_response(); 98 } 99 100 let secret_base32 = base32::encode(base32::Alphabet::Rfc4648 { padding: false }, &secret); 101 102 info!(did = %&auth.0.did, "TOTP secret created (pending verification)"); 103 104 Json(CreateTotpSecretResponse { 105 secret: secret_base32, 106 uri, 107 qr_base64: qr_code, 108 }) 109 .into_response() 110} 111 112#[derive(Deserialize)] 113pub struct EnableTotpInput { 114 pub code: String, 115} 116 117#[derive(Serialize)] 118#[serde(rename_all = "camelCase")] 119pub struct EnableTotpResponse { 120 pub backup_codes: Vec<String>, 121} 122 123pub async fn enable_totp( 124 State(state): State<AppState>, 125 auth: BearerAuth, 126 Json(input): Json<EnableTotpInput>, 127) -> Response { 128 if !state 129 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 130 .await 131 { 132 warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 133 return ApiError::RateLimitExceeded(None).into_response(); 134 } 135 136 let totp_row = sqlx::query!( 137 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 138 &auth.0.did 139 ) 140 .fetch_optional(&state.db) 141 .await; 142 143 let totp_row = match totp_row { 144 Ok(Some(row)) => row, 145 Ok(None) => return ApiError::TotpNotEnabled.into_response(), 146 Err(e) => { 147 error!("DB error fetching TOTP: {:?}", e); 148 return ApiError::InternalError(None).into_response(); 149 } 150 }; 151 152 if totp_row.verified { 153 return ApiError::TotpAlreadyEnabled.into_response(); 154 } 155 156 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) 157 { 158 Ok(s) => s, 159 Err(e) => { 160 error!("Failed to decrypt TOTP secret: {:?}", e); 161 return ApiError::InternalError(None).into_response(); 162 } 163 }; 164 165 let code = input.code.trim(); 166 if !verify_totp_code(&secret, code) { 167 return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 168 } 169 170 let backup_codes = generate_backup_codes(); 171 let mut tx = match state.db.begin().await { 172 Ok(tx) => tx, 173 Err(e) => { 174 error!("Failed to begin transaction: {:?}", e); 175 return ApiError::InternalError(None).into_response(); 176 } 177 }; 178 179 if let Err(e) = sqlx::query!( 180 "UPDATE user_totp SET verified = true, last_used = NOW() WHERE did = $1", 181 &auth.0.did 182 ) 183 .execute(&mut *tx) 184 .await 185 { 186 error!("Failed to enable TOTP: {:?}", e); 187 return ApiError::InternalError(None).into_response(); 188 } 189 190 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 191 .execute(&mut *tx) 192 .await 193 { 194 error!("Failed to clear old backup codes: {:?}", e); 195 return ApiError::InternalError(None).into_response(); 196 } 197 198 for code in &backup_codes { 199 let hash = match hash_backup_code(code) { 200 Ok(h) => h, 201 Err(e) => { 202 error!("Failed to hash backup code: {:?}", e); 203 return ApiError::InternalError(None).into_response(); 204 } 205 }; 206 207 if let Err(e) = sqlx::query!( 208 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())", 209 &auth.0.did, 210 hash 211 ) 212 .execute(&mut *tx) 213 .await 214 { 215 error!("Failed to store backup code: {:?}", e); 216 return ApiError::InternalError(None).into_response(); 217 } 218 } 219 220 if let Err(e) = tx.commit().await { 221 error!("Failed to commit transaction: {:?}", e); 222 return ApiError::InternalError(None).into_response(); 223 } 224 225 info!(did = %&auth.0.did, "TOTP enabled with {} backup codes", backup_codes.len()); 226 227 Json(EnableTotpResponse { backup_codes }).into_response() 228} 229 230#[derive(Deserialize)] 231pub struct DisableTotpInput { 232 pub password: PlainPassword, 233 pub code: String, 234} 235 236pub async fn disable_totp( 237 State(state): State<AppState>, 238 auth: BearerAuth, 239 Json(input): Json<DisableTotpInput>, 240) -> Response { 241 if !crate::api::server::reauth::check_legacy_session_mfa(&state.db, &auth.0.did).await { 242 return crate::api::server::reauth::legacy_mfa_required_response(&state.db, &auth.0.did) 243 .await; 244 } 245 246 if !state 247 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 248 .await 249 { 250 warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 251 return ApiError::RateLimitExceeded(None).into_response(); 252 } 253 254 let user = sqlx::query!( 255 "SELECT password_hash FROM users WHERE did = $1", 256 &*&auth.0.did 257 ) 258 .fetch_optional(&state.db) 259 .await; 260 261 let password_hash = match user { 262 Ok(Some(row)) => row.password_hash, 263 Ok(None) => return ApiError::AccountNotFound.into_response(), 264 Err(e) => { 265 error!("DB error fetching user: {:?}", e); 266 return ApiError::InternalError(None).into_response(); 267 } 268 }; 269 270 let password_valid = password_hash 271 .as_ref() 272 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false)) 273 .unwrap_or(false); 274 if !password_valid { 275 return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 276 } 277 278 let totp_row = sqlx::query!( 279 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 280 &auth.0.did 281 ) 282 .fetch_optional(&state.db) 283 .await; 284 285 let totp_row = match totp_row { 286 Ok(Some(row)) if row.verified => row, 287 Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 288 Err(e) => { 289 error!("DB error fetching TOTP: {:?}", e); 290 return ApiError::InternalError(None).into_response(); 291 } 292 }; 293 294 let code = input.code.trim(); 295 let code_valid = if is_backup_code_format(code) { 296 verify_backup_code_for_user(&state, &auth.0.did, code).await 297 } else { 298 let secret = 299 match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) { 300 Ok(s) => s, 301 Err(e) => { 302 error!("Failed to decrypt TOTP secret: {:?}", e); 303 return ApiError::InternalError(None).into_response(); 304 } 305 }; 306 verify_totp_code(&secret, code) 307 }; 308 309 if !code_valid { 310 return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 311 } 312 313 let mut tx = match state.db.begin().await { 314 Ok(tx) => tx, 315 Err(e) => { 316 error!("Failed to begin transaction: {:?}", e); 317 return ApiError::InternalError(None).into_response(); 318 } 319 }; 320 321 if let Err(e) = sqlx::query!("DELETE FROM user_totp WHERE did = $1", &*&auth.0.did) 322 .execute(&mut *tx) 323 .await 324 { 325 error!("Failed to delete TOTP: {:?}", e); 326 return ApiError::InternalError(None).into_response(); 327 } 328 329 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 330 .execute(&mut *tx) 331 .await 332 { 333 error!("Failed to delete backup codes: {:?}", e); 334 return ApiError::InternalError(None).into_response(); 335 } 336 337 if let Err(e) = tx.commit().await { 338 error!("Failed to commit transaction: {:?}", e); 339 return ApiError::InternalError(None).into_response(); 340 } 341 342 info!(did = %&auth.0.did, "TOTP disabled"); 343 344 EmptyResponse::ok().into_response() 345} 346 347#[derive(Serialize)] 348#[serde(rename_all = "camelCase")] 349pub struct GetTotpStatusResponse { 350 pub enabled: bool, 351 pub has_backup_codes: bool, 352 pub backup_codes_remaining: i64, 353} 354 355pub async fn get_totp_status(State(state): State<AppState>, auth: BearerAuth) -> Response { 356 let totp_row = sqlx::query!( 357 "SELECT verified FROM user_totp WHERE did = $1", 358 &*&auth.0.did 359 ) 360 .fetch_optional(&state.db) 361 .await; 362 363 let enabled = match totp_row { 364 Ok(Some(row)) => row.verified, 365 Ok(None) => false, 366 Err(e) => { 367 error!("DB error fetching TOTP status: {:?}", e); 368 return ApiError::InternalError(None).into_response(); 369 } 370 }; 371 372 let backup_count_row = sqlx::query!( 373 "SELECT COUNT(*) as count FROM backup_codes WHERE did = $1 AND used_at IS NULL", 374 &auth.0.did 375 ) 376 .fetch_one(&state.db) 377 .await; 378 379 let backup_count = backup_count_row.map(|r| r.count.unwrap_or(0)).unwrap_or(0); 380 381 Json(GetTotpStatusResponse { 382 enabled, 383 has_backup_codes: backup_count > 0, 384 backup_codes_remaining: backup_count, 385 }) 386 .into_response() 387} 388 389#[derive(Deserialize)] 390pub struct RegenerateBackupCodesInput { 391 pub password: PlainPassword, 392 pub code: String, 393} 394 395#[derive(Serialize)] 396#[serde(rename_all = "camelCase")] 397pub struct RegenerateBackupCodesResponse { 398 pub backup_codes: Vec<String>, 399} 400 401pub async fn regenerate_backup_codes( 402 State(state): State<AppState>, 403 auth: BearerAuth, 404 Json(input): Json<RegenerateBackupCodesInput>, 405) -> Response { 406 if !state 407 .check_rate_limit(RateLimitKind::TotpVerify, &auth.0.did) 408 .await 409 { 410 warn!(did = %&auth.0.did, "TOTP verification rate limit exceeded"); 411 return ApiError::RateLimitExceeded(None).into_response(); 412 } 413 414 let user = sqlx::query!( 415 "SELECT password_hash FROM users WHERE did = $1", 416 &*&auth.0.did 417 ) 418 .fetch_optional(&state.db) 419 .await; 420 421 let password_hash = match user { 422 Ok(Some(row)) => row.password_hash, 423 Ok(None) => return ApiError::AccountNotFound.into_response(), 424 Err(e) => { 425 error!("DB error fetching user: {:?}", e); 426 return ApiError::InternalError(None).into_response(); 427 } 428 }; 429 430 let password_valid = password_hash 431 .as_ref() 432 .map(|h| bcrypt::verify(&input.password, h).unwrap_or(false)) 433 .unwrap_or(false); 434 if !password_valid { 435 return ApiError::InvalidPassword("Password is incorrect".into()).into_response(); 436 } 437 438 let totp_row = sqlx::query!( 439 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 440 &auth.0.did 441 ) 442 .fetch_optional(&state.db) 443 .await; 444 445 let totp_row = match totp_row { 446 Ok(Some(row)) if row.verified => row, 447 Ok(Some(_)) | Ok(None) => return ApiError::TotpNotEnabled.into_response(), 448 Err(e) => { 449 error!("DB error fetching TOTP: {:?}", e); 450 return ApiError::InternalError(None).into_response(); 451 } 452 }; 453 454 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) 455 { 456 Ok(s) => s, 457 Err(e) => { 458 error!("Failed to decrypt TOTP secret: {:?}", e); 459 return ApiError::InternalError(None).into_response(); 460 } 461 }; 462 463 let code = input.code.trim(); 464 if !verify_totp_code(&secret, code) { 465 return ApiError::InvalidCode(Some("Invalid verification code".into())).into_response(); 466 } 467 468 let backup_codes = generate_backup_codes(); 469 let mut tx = match state.db.begin().await { 470 Ok(tx) => tx, 471 Err(e) => { 472 error!("Failed to begin transaction: {:?}", e); 473 return ApiError::InternalError(None).into_response(); 474 } 475 }; 476 477 if let Err(e) = sqlx::query!("DELETE FROM backup_codes WHERE did = $1", &*&auth.0.did) 478 .execute(&mut *tx) 479 .await 480 { 481 error!("Failed to clear old backup codes: {:?}", e); 482 return ApiError::InternalError(None).into_response(); 483 } 484 485 for code in &backup_codes { 486 let hash = match hash_backup_code(code) { 487 Ok(h) => h, 488 Err(e) => { 489 error!("Failed to hash backup code: {:?}", e); 490 return ApiError::InternalError(None).into_response(); 491 } 492 }; 493 494 if let Err(e) = sqlx::query!( 495 "INSERT INTO backup_codes (did, code_hash, created_at) VALUES ($1, $2, NOW())", 496 &auth.0.did, 497 hash 498 ) 499 .execute(&mut *tx) 500 .await 501 { 502 error!("Failed to store backup code: {:?}", e); 503 return ApiError::InternalError(None).into_response(); 504 } 505 } 506 507 if let Err(e) = tx.commit().await { 508 error!("Failed to commit transaction: {:?}", e); 509 return ApiError::InternalError(None).into_response(); 510 } 511 512 info!(did = %&auth.0.did, "Backup codes regenerated"); 513 514 Json(RegenerateBackupCodesResponse { backup_codes }).into_response() 515} 516 517async fn verify_backup_code_for_user(state: &AppState, did: &str, code: &str) -> bool { 518 let code = code.trim().to_uppercase(); 519 520 let backup_codes = sqlx::query!( 521 "SELECT id, code_hash FROM backup_codes WHERE did = $1 AND used_at IS NULL", 522 did 523 ) 524 .fetch_all(&state.db) 525 .await; 526 527 let backup_codes = match backup_codes { 528 Ok(codes) => codes, 529 Err(e) => { 530 warn!("Failed to fetch backup codes: {:?}", e); 531 return false; 532 } 533 }; 534 535 for row in backup_codes { 536 if verify_backup_code(&code, &row.code_hash) { 537 let _ = sqlx::query!( 538 "UPDATE backup_codes SET used_at = $1 WHERE id = $2", 539 Utc::now(), 540 row.id 541 ) 542 .execute(&state.db) 543 .await; 544 return true; 545 } 546 } 547 548 false 549} 550 551pub async fn verify_totp_or_backup_for_user(state: &AppState, did: &str, code: &str) -> bool { 552 let code = code.trim(); 553 554 if is_backup_code_format(code) { 555 return verify_backup_code_for_user(state, did, code).await; 556 } 557 558 let totp_row = sqlx::query!( 559 "SELECT secret_encrypted, encryption_version, verified FROM user_totp WHERE did = $1", 560 did 561 ) 562 .fetch_optional(&state.db) 563 .await; 564 565 let totp_row = match totp_row { 566 Ok(Some(row)) if row.verified => row, 567 _ => return false, 568 }; 569 570 let secret = match decrypt_totp_secret(&totp_row.secret_encrypted, totp_row.encryption_version) 571 { 572 Ok(s) => s, 573 Err(_) => return false, 574 }; 575 576 if verify_totp_code(&secret, code) { 577 let _ = sqlx::query!("UPDATE user_totp SET last_used = NOW() WHERE did = $1", did) 578 .execute(&state.db) 579 .await; 580 return true; 581 } 582 583 false 584} 585 586pub async fn has_totp_enabled(state: &AppState, did: &str) -> bool { 587 has_totp_enabled_db(&state.db, did).await 588} 589 590pub async fn has_totp_enabled_db(db: &sqlx::PgPool, did: &str) -> bool { 591 let result = sqlx::query_scalar!("SELECT verified FROM user_totp WHERE did = $1", did) 592 .fetch_optional(db) 593 .await; 594 595 matches!(result, Ok(Some(true))) 596}