half impl done

This commit is contained in:
Artemis 2025-03-18 14:20:56 +01:00
parent 77a681c02a
commit 62c9ea0855
8 changed files with 202 additions and 3 deletions

41
Cargo.lock generated
View file

@ -229,6 +229,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "base32"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
[[package]]
name = "base64"
version = "0.21.7"
@ -431,6 +437,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
[[package]]
name = "cookie"
version = "0.18.1"
@ -654,6 +666,7 @@ dependencies = [
"rocket_dyn_templates",
"serde_json",
"sqlx",
"totp-rs",
"uuid",
]
@ -2068,6 +2081,17 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
[[package]]
name = "qrcodegen-image"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "221b7eace1aef8c95d65dbe09fb7a1a43d006045394a89afba6997721fcb7708"
dependencies = [
"base64 0.22.1",
"image",
"qrcodegen",
]
[[package]]
name = "quote"
version = "1.0.38"
@ -3110,6 +3134,23 @@ dependencies = [
"winnow",
]
[[package]]
name = "totp-rs"
version = "5.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90"
dependencies = [
"base32",
"constant_time_eq",
"hmac",
"qrcodegen-image",
"rand",
"sha1",
"sha2",
"url",
"urlencoding",
]
[[package]]
name = "tower-service"
version = "0.3.3"

View file

@ -23,3 +23,4 @@ orion = "0.17"
uuid = { version = "1.11", features = ["serde"] }
clap = { version = "4.5", features = ["derive"] }
qrcode-generator = "5"
totp-rs = { version = "5.6", features = ["gen_secret", "qr"] }

View file

@ -371,6 +371,18 @@ input#ident {
font-size: 2em;
}
.totp-qrcode>img {
display: block;
margin: 0 auto;
height: 160pt;
}
.totp-form input {
width: 100%;
text-align: center;
margin-bottom: 1em;
}
@media screen and (max-width: 400px) {
header.padded>nav {
display: flex;
@ -405,4 +417,4 @@ input#ident {
div.fields.submit {
align-items: center;
}
}
}

View file

@ -1,2 +1,3 @@
pub mod otp;
pub mod pw;
pub mod session;

53
src/auth/otp.rs Normal file
View file

@ -0,0 +1,53 @@
use rocket::{
http::{Cookie, CookieJar},
time::Duration,
};
use totp_rs::{Secret, TOTP};
const ISSUER: &'static str = "dolltags";
fn make_cookie<'a>(value: String) -> Cookie<'a> {
Cookie::build(("otp_init", value))
.max_age(Duration::minutes(5))
.http_only(true)
.same_site(rocket::http::SameSite::Strict)
.path("/account/settings")
.build()
}
/// Creates a TOTP instance using in-code defaults and user OTP settings.
/// i use the internal user account UUID instead of their username to cover the case
/// where a user needs to change their account username.
pub fn make_totp(account_id: &str, secret: Vec<u8>) -> Result<TOTP, totp_rs::TotpUrlError> {
TOTP::new(
totp_rs::Algorithm::SHA1,
6,
1,
30,
secret,
Some(String::from(ISSUER)),
String::from(account_id),
)
}
/// Adds the provided OTP secret as encrypted cookie for 5 minutes
pub fn cache_secret<'a>(cookies: &CookieJar<'a>, secret: &'a Secret) {
if let Secret::Encoded(b32) = secret.to_encoded() {
cookies.add_private(make_cookie(b32));
}
}
/// Tries to get a potentially existing secret from the cookies.
/// If no cookie is set or if the cookie's value doesn't parse to
/// a valid base32 secret this returns None
pub fn get_secret(cookies: &CookieJar<'_>) -> Option<Secret> {
cookies.get_private("otp_init").and_then(|cookie| {
let val = cookie.value_trimmed();
Secret::Encoded(String::from(val)).to_raw().ok()
})
}
/// Manually revokes the encrypted cookie
pub fn remove_secret(cookies: &CookieJar<'_>) {
cookies.remove_private(make_cookie(String::from("woof")))
}

View file

@ -83,6 +83,7 @@ fn rocket() -> _ {
account::common::confirm_terminate_account,
account::common::export_data,
account::otp::show_otp_enable_start,
account::otp::handle_otp_enable_start,
],
)
.mount(

View file

@ -1,6 +1,70 @@
use rocket::{form::Form, http::CookieJar, response::Redirect};
use rocket_dyn_templates::{context, Template};
use totp_rs::Secret;
use crate::{
auth,
db::{
self,
schema::{DollTagsDb, User},
},
pages::CommonTemplateState,
routes,
routes::error_handlers::PageResult,
};
#[get("/settings/otp")]
pub fn show_otp_enable_start() -> Template {
Template::render("account/otp/start", context! {})
pub async fn show_otp_enable_start(
mut db: DollTagsDb,
user: User,
cookies: &CookieJar<'_>,
meta: CommonTemplateState,
) -> PageResult {
if db::otp::has_otp(&mut *db, &user.id).await? {
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
}
let totp_secret = Secret::generate_secret();
auth::otp::cache_secret(cookies, &totp_secret);
let totp = auth::otp::make_totp(&user.id.to_string(), totp_secret.to_bytes()?)?;
let totp_qrcode = totp.get_qr_base64()?;
Ok(Template::render(
"account/otp/start",
context! {
meta,
totp_qrcode,
secret: totp.get_secret_base32(),
},
)
.into())
}
#[derive(FromForm)]
pub struct OtpEnableForm {
#[field(validate = len(6..=6))]
pub otp_code: String,
}
#[post("/settings/otp", data = "<form>")]
pub async fn handle_otp_enable_start(
mut db: DollTagsDb,
form: Form<OtpEnableForm>,
user: User,
cookies: &CookieJar<'_>,
_meta: CommonTemplateState,
) -> PageResult {
if db::otp::has_otp(&mut *db, &user.id).await? {
return Ok(Redirect::to(uri!("/account", routes::account::settings::show_settings)).into());
}
let secret = match auth::otp::get_secret(cookies) {
Some(v) => v,
None => return Ok(Redirect::to(uri!("/account", show_otp_enable_start)).into()),
};
let totp = auth::otp::make_totp(&user.id.to_string(), secret.to_bytes()?)?;
if !totp.check_current(&form.otp_code)? {}
todo!("meow")
}

View file

@ -0,0 +1,26 @@
{% extends "base" %}
{% import "macros/form" as form %}
{% block title %}Adding a one-time password - {% endblock title %}
{% block main %}
<p>To add a one-time password provider, scan this QrCode with your authenticator app.</p>
<section class="split">
<div class="totp-qrcode">
<img src="data:image/png;base64, {{totp_qrcode}}" alt="{{secret}}" />
<p class="note">Alternatively, you can copy/paste this code: <code>{{secret}}</code></p>
</div>
<div class="totp-form">
<p><label for="otp_code">
Once it's added on your authenticator app,
enter the generated 6-digit code.
</label></p>
<form method="post">
<input type="text" name="otp_code" id="otp_code" minlength="6" maxlength="6" required />
<button type="submit">Enable 2FA</button>
</form>
</div>
</section>
{% endblock main %}