settings/security: generate paper key

This commit is contained in:
Philippe Loctaux 2023-04-10 15:17:08 +02:00
parent c1daa34f2c
commit a67c7559b9
12 changed files with 183 additions and 4 deletions

10
Cargo.lock generated
View file

@ -935,6 +935,15 @@ dependencies = [
"slab", "slab",
] ]
[[package]]
name = "gen_passphrase"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f20bae32fbc2a12fe5c574fc0a9834ba7f70abe51c8efe315dedc7a07fd58287"
dependencies = [
"nanorand",
]
[[package]] [[package]]
name = "generator" name = "generator"
version = "0.7.3" version = "0.7.3"
@ -1061,6 +1070,7 @@ name = "hash"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"argon2", "argon2",
"gen_passphrase",
"nanoid", "nanoid",
"nanoid-dictionary", "nanoid-dictionary",
"rand_core", "rand_core",

View file

@ -0,0 +1,5 @@
update users
set paper_key = ?
where id is ?

View file

@ -420,6 +420,16 @@
}, },
"query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n business_name,\n business_logo,\n url\n\nfrom settings\n\nwhere id is 0\n" "query": "select id,\n created_at as \"created_at: DateTime<Utc>\",\n updated_at as \"updated_at: DateTime<Utc>\",\n business_name,\n business_logo,\n url\n\nfrom settings\n\nwhere id is 0\n"
}, },
"68cfa3d135eb4cdbdbcb3b943518b4ac09c371af689c444eb439a37f91ecf7a5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Right": 2
}
},
"query": "update users\n\nset paper_key = ?\n\nwhere id is ?"
},
"69752cc2a3fc91d4e7a39e0b167695f431380bd40df9638b5df3534715de04b0": { "69752cc2a3fc91d4e7a39e0b167695f431380bd40df9638b5df3534715de04b0": {
"describe": { "describe": {
"columns": [ "columns": [

View file

@ -135,4 +135,18 @@ impl Users {
Ok((query.rows_affected() == 1).then_some(())) Ok((query.rows_affected() == 1).then_some(()))
} }
pub async fn set_paper_key(
conn: impl SqliteExecutor<'_>,
id: &str,
paper_key: Option<&str>,
) -> Result<Option<()>, Error> {
let query: SqliteQueryResult =
sqlx::query_file!("queries/users/set_paper_key.sql", paper_key, id)
.execute(conn)
.await
.map_err(handle_error)?;
Ok((query.rows_affected() == 1).then_some(()))
}
} }

View file

@ -39,7 +39,8 @@ impl Icon {
"alert-triangle-large", AlertTriangleLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-alert-triangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>"#, "alert-triangle-large", AlertTriangleLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-alert-triangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>"#,
"id-badge-2", IdBadge2, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-id-badge-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 12h3v4h-3z"></path><path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6"></path><path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 16h2"></path><path d="M14 12h4"></path></svg>"#, "id-badge-2", IdBadge2, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-id-badge-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M7 12h3v4h-3z"></path><path d="M10 6h-6a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h16a1 1 0 0 0 1 -1v-12a1 1 0 0 0 -1 -1h-6"></path><path d="M10 3m0 1a1 1 0 0 1 1 -1h2a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1z"></path><path d="M14 16h2"></path><path d="M14 12h4"></path></svg>"#,
"user", User, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path></svg>"#, "user", User, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-user" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0"></path><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2"></path></svg>"#,
"at", At, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-at" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28"></path></svg>"# "at", At, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-at" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28"></path></svg>"#,
"paperclip-large", PaperclipLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-paperclip" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5"></path></svg>"#
} }
} }
@ -60,6 +61,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
Icon::IdBadge2, Icon::IdBadge2,
Icon::User, Icon::User,
Icon::At, Icon::At,
Icon::PaperclipLarge,
]; ];
// For each icon, it will output: ("icons/name", "<svg>...</svg>") // For each icon, it will output: ("icons/name", "<svg>...</svg>")

View file

@ -13,6 +13,7 @@ pub fn routes() -> Vec<Route> {
user_settings_personal_form, user_settings_personal_form,
user_settings_security, user_settings_security,
user_settings_security_logout_everywhere, user_settings_security_logout_everywhere,
user_settings_security_paper_key,
] ]
} }

View file

@ -4,6 +4,7 @@ use crate::tokens::{
JWT_COOKIE_NAME, JWT_DURATION_MINUTES, REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_DURATION_DAYS, JWT_COOKIE_NAME, JWT_DURATION_MINUTES, REFRESH_TOKEN_COOKIE_NAME, REFRESH_TOKEN_DURATION_DAYS,
}; };
use apps::App; use apps::App;
use hash::PaperKey;
use jwt::database::Key; use jwt::database::Key;
use jwt::PrivateKey; use jwt::PrivateKey;
use refresh_tokens::RefreshToken; use refresh_tokens::RefreshToken;
@ -126,3 +127,52 @@ pub async fn user_settings_security_logout_everywhere(
flash_message, flash_message,
)) ))
} }
#[derive(Debug, FromForm)]
pub struct PaperKeyForm {
pub generate_paper_key: bool,
}
#[post("/settings/security/paper_key", data = "<form>")]
pub async fn user_settings_security_paper_key(
mut db: Connection<Database>,
jwt_user: JwtUser,
form: Form<PaperKeyForm>,
) -> Result<Flash<Redirect>> {
let (flash_kind, flash_message) = if form.generate_paper_key {
// Create new paper key
let paper_key = task::spawn_blocking(PaperKey::generate).await??;
let mut transaction = db.begin().await?;
// Get user info
let user = User::get_by_login(&mut transaction, &jwt_user.0.subject)
.await?
.ok_or_else(|| Error::not_found("Could not find user"))?;
// Save paper key
user.set_paper_key(&mut transaction, Some(&paper_key))
.await?;
transaction.commit().await?;
// Safety: safe to unwrap, the value is present
let plain_paper_key = paper_key.plain().unwrap();
(
FlashKind::Success,
format!(
"Your paper key has been generated. It will only be shown once!\
<div class=\"mt-1 user-select-all\">{plain_paper_key}</div>"
),
)
} else {
(FlashKind::Warning, "Nothing to do.".into())
};
Ok(Flash::new(
Redirect::to(uri!(user_settings_security)),
flash_kind,
flash_message,
))
}

View file

@ -55,7 +55,7 @@
<p class="card-subtitle"> <p class="card-subtitle">
You can use your paper key to reset your password if you forget it.</p> You can use your paper key to reset your password if you forget it.</p>
<div> <div>
<a href="#" class="btn"> <a class="btn" data-bs-toggle="modal" data-bs-target="#modal-paper-key">
Generate new paper key Generate new paper key
</a> </a>
</div> </div>
@ -79,6 +79,51 @@
</div> </div>
</div> </div>
<!-- Paper key modal -->
<div class="modal modal-blur" tabindex="-1" id="modal-paper-key">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-status bg-info"></div>
<div class="modal-body text-center py-4">
<div class="text-info mb-2">
{% include "icons/paperclip-large" %}
</div>
<h3>Generate new paper key</h3>
<div class="mt-2">A new paper key will be generated for your account.</div>
<div class="mt-2">This action will revoke your previous paper key if you had one.</div>
<div class="mt-2">Keep your paper key in a safe place!</div>
</div>
<div class="modal-footer">
<div class="w-100">
<div class="row">
<div class="col">
<a href="#" class="btn w-100" data-bs-dismiss="modal">Cancel</a>
</div>
<div class="col">
<form action="./security/paper_key" method="post">
<button type="submit" name="generate_paper_key" value="true"
class="btn btn-primary w-100">
Generate
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Logout everywhere modal --> <!-- Logout everywhere modal -->
<div class="modal modal-blur" tabindex="-1" id="modal-logout-confirm"> <div class="modal modal-blur" tabindex="-1" id="modal-logout-confirm">
<div class="modal-dialog modal-sm modal-dialog-centered" role="document"> <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
@ -108,7 +153,8 @@
<div class="col"> <div class="col">
<form action="./security/logout_everywhere" method="post"> <form action="./security/logout_everywhere" method="post">
<button type="submit" name="logout_everywhere" value="true" class="btn btn-danger w-100"> <button type="submit" name="logout_everywhere" value="true"
class="btn btn-danger w-100">
Logout everywhere Logout everywhere
</button> </button>
</form> </form>

View file

@ -9,3 +9,4 @@ rand_core = { version = "0.6", features = ["std"] }
thiserror = { workspace = true } thiserror = { workspace = true }
nanoid = { workspace = true } nanoid = { workspace = true }
nanoid-dictionary = { workspace = true } nanoid-dictionary = { workspace = true }
gen_passphrase = { version = "0.1", features = ["eff_short_2"] }

View file

@ -1,8 +1,10 @@
mod error; mod error;
mod hash; mod hash;
mod paper_key;
mod password; mod password;
mod secret; mod secret;
pub use error::Error; pub use error::Error;
pub use paper_key::PaperKey;
pub use password::Password; pub use password::Password;
pub use secret::{Secret, SecretString}; pub use secret::{Secret, SecretString};

View file

@ -0,0 +1,26 @@
use crate::error::Error;
use crate::hash::Hash;
#[derive(Debug)]
pub struct PaperKey(Hash);
impl PaperKey {
pub fn generate() -> Result<Self, Error> {
use gen_passphrase::{dictionary::EFF_SHORT_2, generate};
let passphrase = generate(&[EFF_SHORT_2], 7, Some("-"));
Ok(Self(Hash::from_plain(passphrase)?))
}
pub fn from_hash(hash: impl Into<String>) -> Self {
Self(Hash::from_hash(hash))
}
pub fn plain(&self) -> Option<&str> {
self.0.plain()
}
pub fn hash(&self) -> &str {
self.0.hash()
}
pub fn compare(&self, plain: &str) -> Result<bool, Error> {
self.0.compare(plain).map_err(Error::from)
}
}

View file

@ -4,7 +4,7 @@ use database::sqlx::SqliteExecutor;
use database::Error as DatabaseError; use database::Error as DatabaseError;
use database::Users as DatabaseUsers; use database::Users as DatabaseUsers;
use email_address::EmailAddress; use email_address::EmailAddress;
use hash::Password; use hash::{PaperKey, Password};
use id::UserID; use id::UserID;
use std::str::FromStr; use std::str::FromStr;
@ -155,4 +155,16 @@ impl User {
Ok(()) Ok(())
} }
pub async fn set_paper_key(
&self,
conn: impl SqliteExecutor<'_>,
paper_key: Option<&PaperKey>,
) -> Result<(), Error> {
let paper_key = paper_key.map(|paper_key| paper_key.hash());
DatabaseUsers::set_paper_key(conn, self.id.as_ref(), paper_key).await?;
Ok(())
}
} }