feat: WIP! password strength indicator

This commit is contained in:
Max Hohlfeld 2024-08-18 23:21:18 +02:00
parent b84b2bd615
commit e3c1aab6e2
4 changed files with 205 additions and 3 deletions

137
Cargo.lock generated
View File

@ -707,6 +707,21 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@ -776,6 +791,7 @@ dependencies = [
"serde_json", "serde_json",
"sqlx", "sqlx",
"static-files", "static-files",
"zxcvbn",
] ]
[[package]] [[package]]
@ -1010,6 +1026,41 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "darling"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.72",
]
[[package]]
name = "darling_macro"
version = "0.20.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
dependencies = [
"darling_core",
"quote",
"syn 2.0.72",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -1019,6 +1070,37 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive_builder"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.72",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b"
dependencies = [
"derive_builder_core",
"syn 2.0.72",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.18" version = "0.99.18"
@ -1160,6 +1242,17 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "fancy-regex"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "1.9.0" version = "1.9.0"
@ -1543,6 +1636,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "0.5.0"
@ -1602,6 +1701,15 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -1641,6 +1749,12 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.11.7" version = "0.11.7"
@ -2641,6 +2755,12 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -3259,3 +3379,20 @@ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
] ]
[[package]]
name = "zxcvbn"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad76e35b00ad53688d6b90c431cabe3cbf51f7a4a154739e04b63004ab1c736c"
dependencies = [
"chrono",
"derive_builder",
"fancy-regex",
"itertools",
"lazy_static",
"regex",
"time",
"wasm-bindgen",
"web-sys",
]

View File

@ -28,6 +28,7 @@ lettre = "0.11.7"
quick-xml = { version = "0.31.0", features = ["serde", "serialize"] } quick-xml = { version = "0.31.0", features = ["serde", "serialize"] }
actix-web-static-files = "4.0" actix-web-static-files = "4.0"
static-files = "0.2.1" static-files = "0.2.1"
zxcvbn = "3.1.0"
[build-dependencies] [build-dependencies]
built = "0.7.4" built = "0.7.4"

View File

@ -7,6 +7,10 @@ use lettre::{
}; };
use serde::Deserialize; use serde::Deserialize;
use sqlx::PgPool; use sqlx::PgPool;
use zxcvbn::{
feedback::{Suggestion, Warning},
zxcvbn, Score,
};
use crate::{ use crate::{
auth, auth,
@ -19,6 +23,7 @@ struct ResetPasswordForm {
token: Option<String>, token: Option<String>,
password: Option<String>, password: Option<String>,
passwordretyped: Option<String>, passwordretyped: Option<String>,
dry: Option<bool>,
} }
#[actix_web::post("/reset-password")] #[actix_web::post("/reset-password")]
@ -91,6 +96,63 @@ Viele Grüße"##, user.name, reset_url))
return HttpResponse::BadRequest().body("Token existiert nicht bzw. ist abgelaufen!"); return HttpResponse::BadRequest().body("Token existiert nicht bzw. ist abgelaufen!");
} }
let user = User::read_by_id(pool.get_ref(), token.as_ref().unwrap().id)
.await
.unwrap();
let mut split_names: Vec<&str> = user.name.as_str().split_whitespace().collect();
let mut user_inputs = vec![user.email.as_str()];
user_inputs.append(&mut split_names);
let entropy = zxcvbn(form.password.as_ref().unwrap(), &user_inputs);
if entropy.score() < Score::Three {
let feedback = entropy.feedback().unwrap();
let warning = match feedback.warning() {
Some(Warning::ThisIsATop10Password) => {
"Das Passwort ist eins der 10 meist genutzten."
}
Some(Warning::ThisIsATop100Password) => {
"Das Passwort ist eins der 100 meist genutzten."
}
Some(Warning::ThisIsACommonPassword) => "Das ist ein zu übliches Password.",
Some(Warning::DatesAreOftenEasyToGuess) => {
"Datumsangaben lassen sich meist leicht erraten."
}
Some(Warning::RecentYearsAreEasyToGuess) => {
"Vergangene Jahreszahlen lassen sich leicht erraten."
}
Some(Warning::AWordByItselfIsEasyToGuess) => "abc",
Some(Warning::RepeatsLikeAaaAreEasyToGuess) => "abc",
Some(Warning::SequencesLikeAbcAreEasyToGuess) => "abc",
Some(Warning::StraightRowsOfKeysAreEasyToGuess) => "abc",
Some(Warning::ShortKeyboardPatternsAreEasyToGuess) => "abc",
Some(Warning::ThisIsSimilarToACommonlyUsedPassword) => "abc",
Some(Warning::CommonNamesAndSurnamesAreEasyToGuess) => "abc",
Some(Warning::NamesAndSurnamesByThemselvesAreEasyToGuess) => "abc",
Some(Warning::RepeatsLikeAbcAbcAreOnlySlightlyHarderToGuess) => "abc",
_ => "",
};
let suggestion = feedback
.suggestions()
.iter()
.map(|s| match s {
Suggestion::AvoidSequences => "abc",
Suggestion::AvoidRecentYears => "abc",
_ => "abc",
})
.collect::<Vec<&str>>()
.join("</br>");
return HttpResponse::BadRequest().body(format!("Passwort zu schwach.</br>Warnung: {warning}</br>Vorschlag: {suggestion}"));
}
if form.dry.is_some_and(|b| b) {
return HttpResponse::NoContent().finish();
}
if form.password.as_ref().unwrap() != form.passwordretyped.as_ref().unwrap() { if form.password.as_ref().unwrap() != form.passwordretyped.as_ref().unwrap() {
return HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!"); return HttpResponse::BadRequest().body("Passwörter stimmen nicht überein!");
} }

View File

@ -4,20 +4,22 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<h1 class="title">Brass - Passwort zurücksetzen</h1> <h1 class="title">Brass - Passwort zurücksetzen</h1>
<form class="box" hx-post="/reset-password" hx-target-400="#error-message" hx-on:change="document.getElementById('error-message').innerHTML = ''"> <form class="box" hx-post="/reset-password" hx-params="not dry" hx-target-400="#error-message" hx-on:change="document.getElementById('error-message').innerHTML = ''">
<input type="hidden" name="token" value="{{ token }}"/> <input type="hidden" name="token" value="{{ token }}"/>
<input type="hidden" name="dry" value="true"/>
<div class="field"> <div class="field">
<label class="label" for="password">neues Passwort:</label> <label class="label" for="password">neues Passwort:</label>
<div class="control"> <div class="control">
<input class="input" placeholder="**********" name="password" type="password" required> <input class="input" hx-post="/reset-password?dry=true" hx-params="*" hx-trigger="keyup changed delay:1s" hx-target-400="#error-message" placeholder="**********" name="password" type="password" required>
</div> </div>
</div> </div>
<div class="field"> <div class="field">
<label class="label" for="passwordretyped">neues Passwort wiederholen:</label> <label class="label" for="passwordretyped">neues Passwort wiederholen:</label>
<div class="control"> <div class="control">
<input class="input" placeholder="**********" name="passwordretyped" type="password" required> <input class="input" placeholder="**********" name="passwordretyped" type="password" max=256 required>
</div> </div>
</div> </div>