diff --git a/assets/site.css b/assets/site.css index 2167a65..7f7601b 100644 --- a/assets/site.css +++ b/assets/site.css @@ -103,6 +103,7 @@ textarea, border-radius: 4pt; padding: 4pt 8pt; background-color: var(--clr-surface-tonal-a10); + font-size: 1em; } input, @@ -126,6 +127,7 @@ textarea:hover, button:hover, .btn:hover { border-color: var(--clr-primary-a0); + cursor: pointer; } textarea { @@ -157,7 +159,8 @@ section { p.form-error, div.form-error, -a.error { +a.error, +button.error { background-color: var(--clr-error-surface); border: 2pt solid var(--clr-error-primary-0); color: var(--clr-error-primary-40); @@ -165,6 +168,15 @@ a.error { padding: .5em 1em; } +a.error:hover, +button.error:hover, +a.error:focus, +button.error:focus, +a.error:active, +button.error:active { + border-color: var(--clr-error-primary-40); +} + p.note { font-size: .8em; } diff --git a/src/db/otp.rs b/src/db/otp.rs index a213180..ecea1af 100644 --- a/src/db/otp.rs +++ b/src/db/otp.rs @@ -54,3 +54,35 @@ pub async fn add_otp_method( Ok(()) } + +/// Deletes the given OTP method's configuration for the user +pub async fn delete_otp_method(db: &mut DbHook, id: &Uuid, method: &str) -> sqlx::Result<()> { + sqlx::query!( + "delete from otp where user_id = $1 and otp_method = $2", + id, + method + ) + .execute(&mut **db) + .await?; + + Ok(()) +} + +/// Changes the recovery key for the given OTP method; if there was none defined, will do nothing +pub async fn change_recovery_key( + db: &mut DbHook, + id: &Uuid, + otp_method: &str, + hashed_recovery_key: &str, +) -> sqlx::Result<()> { + sqlx::query!( + "update otp set recovery_key = $3 where user_id = $1 and otp_method = $2", + id, + otp_method, + hashed_recovery_key + ) + .execute(&mut **db) + .await?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 66c527e..1376c30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,6 +84,10 @@ fn rocket() -> _ { account::common::export_data, account::otp::show_totp_enable_start, account::otp::handle_totp_enable_start, + account::otp::show_confirm_totp_regenerate_key, + account::otp::regenerate_key, + account::otp::show_confirm_totp_disable, + account::otp::handle_confirm_totp_disable, ], ) .mount( diff --git a/src/routes/account/otp.rs b/src/routes/account/otp.rs index d214808..71546fb 100644 --- a/src/routes/account/otp.rs +++ b/src/routes/account/otp.rs @@ -102,6 +102,8 @@ pub async fn handle_totp_enable_start( .await?; } + auth::otp::remove_secret(cookies); + Ok(Template::render( "account/otp/confirm", context! { @@ -111,3 +113,98 @@ pub async fn handle_totp_enable_start( ) .into()) } + +#[get("/settings/totp/generate-key")] +pub async fn show_confirm_totp_regenerate_key( + mut db: DollTagsDb, + user: User, + meta: CommonTemplateState, +) -> PageResult { + if !db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? { + return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into()); + } + + Ok(Template::render( + "account/otp/confirm_regenerate_key", + context! { + meta, + }, + ) + .into()) +} + +#[post("/settings/totp/generate-key")] +pub async fn regenerate_key( + mut db: DollTagsDb, + user: User, + meta: CommonTemplateState, + client_ip: IpAddr, +) -> PageResult { + if !db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? { + return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into()); + } + + warn!( + "[audit|{}] [{}] regenerated TOTP 2FA recovery key", + client_ip, + user.id.to_string(), + ); + + let recovery_key = generate_recovery_key(); + + { + let recovery_key = recovery_key.clone(); + let hashed_recovery_key = task::spawn_blocking(move || pw::hash(&recovery_key)).await??; + + db::otp::change_recovery_key(&mut *db, &user.id, METHOD_TOTP, &hashed_recovery_key).await?; + } + + Ok(Template::render( + "account/otp/regenerate_key", + context! { + recovery_key, + meta, + }, + ) + .into()) +} + +#[get("/settings/totp/disable")] +pub async fn show_confirm_totp_disable( + mut db: DollTagsDb, + user: User, + meta: CommonTemplateState, +) -> PageResult { + if !db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? { + return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into()); + } + + Ok(Template::render( + "account/otp/disable", + context! { + meta, + }, + ) + .into()) +} + +#[post("/settings/totp/disable")] +pub async fn handle_confirm_totp_disable( + mut db: DollTagsDb, + user: User, + client_ip: IpAddr, +) -> PageResult { + if !db::otp::has_otp(&mut *db, &user.id, METHOD_TOTP).await? { + return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into()); + } + + warn!( + "[audit|{}] [{}] deactivated TOTP 2FA", + client_ip, + user.id.to_string(), + ); + + db::otp::delete_otp_method(&mut *db, &user.id, METHOD_TOTP).await?; + + Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into()) +} diff --git a/templates/account/otp/confirm_regenerate_key.html.tera b/templates/account/otp/confirm_regenerate_key.html.tera new file mode 100644 index 0000000..345f395 --- /dev/null +++ b/templates/account/otp/confirm_regenerate_key.html.tera @@ -0,0 +1,19 @@ +{% extends "base" %} +{% import "macros/form" as form %} +{% block title %}Regenerate your recovery key - {% endblock title %} +{% block main %} +
You're about to regenerate a recovery key.
+ ++ This will invalidate the current 2FA recovery key and generate a new one + which you'll have to store safely. +
+ +You're about to disable TOTP 2FA.
+ ++ TOTP 2FA is an important additional security measure that comes into play when + your password gets compromised by someone. +
++ If you're about to disable it due to having lost your recovery key, know that + you can instead choose to generate a new key. +
+If you still want to disable TOTP 2FA on your account, click on the red button below.
+ +Here's your new recovery key.
+ +{{recovery_key}}
+
+Make sure to save it somewhere safe, it may come in handy.
+Finish and go back to settings
+{% endblock main %} \ No newline at end of file diff --git a/templates/account/settings.html.tera b/templates/account/settings.html.tera index 5f126a2..f3916a7 100644 --- a/templates/account/settings.html.tera +++ b/templates/account/settings.html.tera @@ -67,18 +67,26 @@Wow, you already have {{method}} OTP enabled even though it's not yet implemented.
- {% else %}You have enabled time-based 2FA, which will prompt you for a code on each login.
+ ++ You may regenerate a recovery key + in case you lost your current one, + or disable TOTP 2FA altogether. +
+ {% endif %} + + {% if enabled_otp_methods|length == 0 %}
You don't have two-factor auth enabled.
You can add one using your authenticator app of choice by clicking below.