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. +

+ +
+
+ +
+ +

Or go back to the settings

+
+{% endblock main %} \ No newline at end of file diff --git a/templates/account/otp/disable.html.tera b/templates/account/otp/disable.html.tera new file mode 100644 index 0000000..9f77a54 --- /dev/null +++ b/templates/account/otp/disable.html.tera @@ -0,0 +1,24 @@ +{% extends "base" %} +{% import "macros/form" as form %} +{% block title %}Disable TOTP 2FA - {% endblock title %} +{% block main %} +

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.

+ +
+
+ +
+ +

Or go back to the settings

+
+{% endblock main %} \ No newline at end of file diff --git a/templates/account/otp/regenerate_key.html.tera b/templates/account/otp/regenerate_key.html.tera new file mode 100644 index 0000000..b5ce74c --- /dev/null +++ b/templates/account/otp/regenerate_key.html.tera @@ -0,0 +1,11 @@ +{% extends "base" %} +{% import "macros/form" as form %} +{% block title %}Regenerate your recovery key - {% endblock title %} +{% block main %} +

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 @@
- {% for method in enabled_otp_methods %} -

Wow, you already have {{method}} OTP enabled even though it's not yet implemented.

- {% else %}

Two-factor authentication

+ {% if "totp" in enabled_otp_methods %} +

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.

Enable 2FA with an authenticator - {% endfor %} + {% endif %}