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