settings/security: generate paper key
This commit is contained in:
parent
c1daa34f2c
commit
a67c7559b9
12 changed files with 183 additions and 4 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
5
crates/database/queries/users/set_paper_key.sql
Normal file
5
crates/database/queries/users/set_paper_key.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update users
|
||||||
|
|
||||||
|
set paper_key = ?
|
||||||
|
|
||||||
|
where id is ?
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
|
|
@ -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(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>")
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"] }
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
26
crates/hash/src/paper_key.rs
Normal file
26
crates/hash/src/paper_key.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue