2019-11-25 06:28:49 +01:00
|
|
|
use chrono::Utc;
|
2021-11-07 18:53:39 +01:00
|
|
|
use rocket::serde::json::Json;
|
2021-03-27 16:07:26 +01:00
|
|
|
use serde_json::Value;
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2020-07-14 18:00:09 +02:00
|
|
|
use crate::{
|
2022-11-20 19:15:45 +01:00
|
|
|
api::{
|
|
|
|
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
|
|
|
|
},
|
|
|
|
auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers},
|
2020-07-14 18:00:09 +02:00
|
|
|
crypto,
|
|
|
|
db::{models::*, DbConn},
|
|
|
|
mail, CONFIG,
|
|
|
|
};
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2020-07-14 18:00:09 +02:00
|
|
|
pub fn routes() -> Vec<rocket::Route> {
|
2018-10-10 20:40:39 +02:00
|
|
|
routes![
|
|
|
|
register,
|
|
|
|
profile,
|
|
|
|
put_profile,
|
|
|
|
post_profile,
|
|
|
|
get_public_keys,
|
|
|
|
post_keys,
|
|
|
|
post_password,
|
|
|
|
post_kdf,
|
2018-11-24 23:00:41 +01:00
|
|
|
post_rotatekey,
|
2018-10-10 20:40:39 +02:00
|
|
|
post_sstamp,
|
|
|
|
post_email_token,
|
|
|
|
post_email,
|
2019-11-25 06:28:49 +01:00
|
|
|
post_verify_email,
|
|
|
|
post_verify_email_token,
|
|
|
|
post_delete_recover,
|
|
|
|
post_delete_recover_token,
|
2018-10-10 20:40:39 +02:00
|
|
|
delete_account,
|
|
|
|
post_delete_account,
|
|
|
|
revision_date,
|
|
|
|
password_hint,
|
|
|
|
prelogin,
|
2020-09-25 18:26:48 +02:00
|
|
|
verify_password,
|
2022-01-19 11:51:26 +01:00
|
|
|
api_key,
|
|
|
|
rotate_api_key,
|
2022-11-06 18:07:09 +01:00
|
|
|
get_known_device,
|
2023-01-11 21:45:11 +01:00
|
|
|
put_avatar,
|
2018-10-10 20:40:39 +02:00
|
|
|
]
|
|
|
|
}
|
|
|
|
|
2018-02-10 01:00:55 +01:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
2022-11-14 17:22:37 +01:00
|
|
|
pub struct RegisterData {
|
2018-06-01 00:18:50 +02:00
|
|
|
Email: String,
|
2018-09-19 17:30:14 +02:00
|
|
|
Kdf: Option<i32>,
|
|
|
|
KdfIterations: Option<i32>,
|
2018-06-01 00:18:50 +02:00
|
|
|
Key: String,
|
|
|
|
Keys: Option<KeysData>,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
MasterPasswordHint: Option<String>,
|
|
|
|
Name: Option<String>,
|
2018-12-21 04:16:41 +01:00
|
|
|
Token: Option<String>,
|
2021-09-22 21:39:31 +02:00
|
|
|
#[allow(dead_code)]
|
2018-12-21 04:16:41 +01:00
|
|
|
OrganizationUserId: Option<String>,
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct KeysData {
|
2018-06-01 00:35:30 +02:00
|
|
|
EncryptedPrivateKey: String,
|
|
|
|
PublicKey: String,
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2022-07-01 05:46:17 +02:00
|
|
|
/// Trims whitespace from password hints, and converts blank password hints to `None`.
|
|
|
|
fn clean_password_hint(password_hint: &Option<String>) -> Option<String> {
|
|
|
|
match password_hint {
|
|
|
|
None => None,
|
|
|
|
Some(h) => match h.trim() {
|
|
|
|
"" => None,
|
|
|
|
ht => Some(ht.to_string()),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn enforce_password_hint_setting(password_hint: &Option<String>) -> EmptyResult {
|
|
|
|
if password_hint.is_some() && !CONFIG.password_hints_allowed() {
|
|
|
|
err!("Password hints have been disabled by the administrator. Remove the hint and try again.");
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2018-02-10 01:00:55 +01:00
|
|
|
#[post("/accounts/register", data = "<data>")]
|
2022-11-14 17:22:37 +01:00
|
|
|
async fn register(data: JsonUpcase<RegisterData>, conn: DbConn) -> JsonResult {
|
|
|
|
_register(data, conn).await
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn _register(data: JsonUpcase<RegisterData>, mut conn: DbConn) -> JsonResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: RegisterData = data.into_inner().data;
|
2021-09-09 13:50:18 +02:00
|
|
|
let email = data.Email.to_lowercase();
|
2018-02-17 23:38:55 +01:00
|
|
|
|
2022-06-08 19:46:33 +02:00
|
|
|
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
|
|
|
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
|
|
|
if let Some(ref name) = data.Name {
|
|
|
|
if name.len() > 50 {
|
|
|
|
err!("The field Name must be a string with a maximum length of 50.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-01 05:46:17 +02:00
|
|
|
// Check against the password hint setting here so if it fails, the user
|
|
|
|
// can retry without losing their invitation below.
|
|
|
|
let password_hint = clean_password_hint(&data.MasterPasswordHint);
|
|
|
|
enforce_password_hint_setting(&password_hint)?;
|
|
|
|
|
2022-10-07 07:26:53 +02:00
|
|
|
let mut verified_by_invite = false;
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
let mut user = match User::find_by_mail(&email, &mut conn).await {
|
2022-10-07 07:26:53 +02:00
|
|
|
Some(mut user) => {
|
2019-01-08 15:11:16 +01:00
|
|
|
if !user.password_hash.is_empty() {
|
2022-10-06 00:18:20 +02:00
|
|
|
err!("Registration not allowed or user already exists")
|
2019-01-08 15:11:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(token) = data.Token {
|
2019-01-19 21:36:34 +01:00
|
|
|
let claims = decode_invite(&token)?;
|
2021-09-09 13:50:18 +02:00
|
|
|
if claims.email == email {
|
2022-10-07 07:26:53 +02:00
|
|
|
// Verify the email address when signing up via a valid invite token
|
|
|
|
verified_by_invite = true;
|
|
|
|
user.verified_at = Some(Utc::now().naive_utc());
|
2018-12-15 03:56:00 +01:00
|
|
|
user
|
|
|
|
} else {
|
2019-01-08 15:11:16 +01:00
|
|
|
err!("Registration email does not match invite email")
|
|
|
|
}
|
2022-05-20 23:39:47 +02:00
|
|
|
} else if Invitation::take(&email, &mut conn).await {
|
|
|
|
for mut user_org in UserOrganization::find_invited_by_user(&user.uuid, &mut conn).await.iter_mut() {
|
2019-01-08 15:11:16 +01:00
|
|
|
user_org.status = UserOrgStatus::Accepted as i32;
|
2022-05-20 23:39:47 +02:00
|
|
|
user_org.save(&mut conn).await?;
|
2018-11-24 23:00:41 +01:00
|
|
|
}
|
2022-01-07 18:55:48 +01:00
|
|
|
user
|
2022-10-06 00:18:20 +02:00
|
|
|
} else if CONFIG.is_signup_allowed(&email)
|
2022-05-20 23:39:47 +02:00
|
|
|
|| EmergencyAccess::find_invited_by_grantee_email(&email, &mut conn).await.is_some()
|
2022-10-06 00:18:20 +02:00
|
|
|
{
|
2019-01-08 15:11:16 +01:00
|
|
|
user
|
2018-09-10 15:51:40 +02:00
|
|
|
} else {
|
2019-11-01 23:34:42 +01:00
|
|
|
err!("Registration not allowed or user already exists")
|
2018-09-10 15:51:40 +02:00
|
|
|
}
|
2018-11-24 23:00:41 +01:00
|
|
|
}
|
2018-09-10 15:51:40 +02:00
|
|
|
None => {
|
2020-04-09 10:42:27 +02:00
|
|
|
// Order is important here; the invitation check must come first
|
2021-04-27 23:18:32 +02:00
|
|
|
// because the vaultwarden admin can invite anyone, regardless
|
2020-04-09 10:42:27 +02:00
|
|
|
// of other signup restrictions.
|
2022-05-20 23:39:47 +02:00
|
|
|
if Invitation::take(&email, &mut conn).await || CONFIG.is_signup_allowed(&email) {
|
2021-09-09 13:50:18 +02:00
|
|
|
User::new(email.clone())
|
2018-09-10 15:51:40 +02:00
|
|
|
} else {
|
2019-11-01 23:34:42 +01:00
|
|
|
err!("Registration not allowed or user already exists")
|
2018-09-10 15:51:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2019-01-08 15:11:16 +01:00
|
|
|
// Make sure we don't leave a lingering invitation.
|
2022-05-20 23:39:47 +02:00
|
|
|
Invitation::take(&email, &mut conn).await;
|
2019-01-08 15:11:16 +01:00
|
|
|
|
2018-09-19 17:30:14 +02:00
|
|
|
if let Some(client_kdf_iter) = data.KdfIterations {
|
|
|
|
user.client_kdf_iter = client_kdf_iter;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(client_kdf_type) = data.Kdf {
|
|
|
|
user.client_kdf_type = client_kdf_type;
|
|
|
|
}
|
|
|
|
|
2023-01-14 10:16:03 +01:00
|
|
|
user.set_password(&data.MasterPasswordHash, Some(data.Key), true, None);
|
2022-07-01 05:46:17 +02:00
|
|
|
user.password_hint = password_hint;
|
2018-02-10 01:00:55 +01:00
|
|
|
|
|
|
|
// Add extra fields if present
|
2018-06-01 00:18:50 +02:00
|
|
|
if let Some(name) = data.Name {
|
2018-02-10 01:00:55 +01:00
|
|
|
user.name = name;
|
|
|
|
}
|
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
if let Some(keys) = data.Keys {
|
2018-06-01 00:35:30 +02:00
|
|
|
user.private_key = Some(keys.EncryptedPrivateKey);
|
|
|
|
user.public_key = Some(keys.PublicKey);
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
if CONFIG.mail_enabled() {
|
2022-10-07 07:26:53 +02:00
|
|
|
if CONFIG.signups_verify() && !verified_by_invite {
|
2022-07-06 23:57:37 +02:00
|
|
|
if let Err(e) = mail::send_welcome_must_verify(&user.email, &user.uuid).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
error!("Error sending welcome email: {:#?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
user.last_verifying_at = Some(user.created_at);
|
2022-07-06 23:57:37 +02:00
|
|
|
} else if let Err(e) = mail::send_welcome(&user.email).await {
|
2021-03-27 15:03:31 +01:00
|
|
|
error!("Error sending welcome email: {:#?}", e);
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
user.save(&mut conn).await?;
|
2022-10-06 21:26:49 +02:00
|
|
|
Ok(Json(json!({
|
2022-10-08 10:27:33 +02:00
|
|
|
"Object": "register",
|
2022-10-07 06:33:29 +02:00
|
|
|
"CaptchaBypassToken": "",
|
2022-10-06 21:26:49 +02:00
|
|
|
})))
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/accounts/profile")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn profile(headers: Headers, mut conn: DbConn) -> Json<Value> {
|
|
|
|
Json(headers.user.to_json(&mut conn).await)
|
2018-04-24 22:01:55 +02:00
|
|
|
}
|
|
|
|
|
2018-06-17 00:06:59 +02:00
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct ProfileData {
|
2022-10-17 17:23:21 +02:00
|
|
|
// Culture: String, // Ignored, always use en-US
|
|
|
|
// MasterPasswordHint: Option<String>, // Ignored, has been moved to ChangePassData
|
2018-06-17 00:06:59 +02:00
|
|
|
Name: String,
|
|
|
|
}
|
|
|
|
|
2018-08-15 17:10:40 +02:00
|
|
|
#[put("/accounts/profile", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn put_profile(data: JsonUpcase<ProfileData>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
|
|
post_profile(data, headers, conn).await
|
2018-08-15 17:10:40 +02:00
|
|
|
}
|
|
|
|
|
2018-06-17 00:06:59 +02:00
|
|
|
#[post("/accounts/profile", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_profile(data: JsonUpcase<ProfileData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
2018-06-17 00:06:59 +02:00
|
|
|
let data: ProfileData = data.into_inner().data;
|
|
|
|
|
2022-06-08 19:46:33 +02:00
|
|
|
// Check if the length of the username exceeds 50 characters (Same is Upstream Bitwarden)
|
|
|
|
// This also prevents issues with very long usernames causing to large JWT's. See #2419
|
|
|
|
if data.Name.len() > 50 {
|
|
|
|
err!("The field Name must be a string with a maximum length of 50.");
|
|
|
|
}
|
|
|
|
|
2018-06-17 00:06:59 +02:00
|
|
|
let mut user = headers.user;
|
|
|
|
user.name = data.Name;
|
2022-07-01 05:46:17 +02:00
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
user.save(&mut conn).await?;
|
|
|
|
Ok(Json(user.to_json(&mut conn).await))
|
2018-06-17 00:06:59 +02:00
|
|
|
}
|
|
|
|
|
2023-01-11 21:45:11 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct AvatarData {
|
|
|
|
AvatarColor: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[put("/accounts/avatar", data = "<data>")]
|
|
|
|
async fn put_avatar(data: JsonUpcase<AvatarData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
|
|
|
let data: AvatarData = data.into_inner().data;
|
|
|
|
|
|
|
|
// It looks like it only supports the 6 hex color format.
|
|
|
|
// If you try to add the short value it will not show that color.
|
|
|
|
// Check and force 7 chars, including the #.
|
|
|
|
if let Some(color) = &data.AvatarColor {
|
|
|
|
if color.len() != 7 {
|
|
|
|
err!("The field AvatarColor must be a HTML/Hex color code with a length of 7 characters")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut user = headers.user;
|
|
|
|
user.avatar_color = data.AvatarColor;
|
|
|
|
|
|
|
|
user.save(&mut conn).await?;
|
|
|
|
Ok(Json(user.to_json(&mut conn).await))
|
|
|
|
}
|
|
|
|
|
2018-04-24 22:01:55 +02:00
|
|
|
#[get("/users/<uuid>/public-key")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn get_public_keys(uuid: String, _headers: Headers, mut conn: DbConn) -> JsonResult {
|
|
|
|
let user = match User::find_by_uuid(&uuid, &mut conn).await {
|
2018-04-24 22:01:55 +02:00
|
|
|
Some(user) => user,
|
2018-11-24 23:00:41 +01:00
|
|
|
None => err!("User doesn't exist"),
|
2018-04-24 22:01:55 +02:00
|
|
|
};
|
|
|
|
|
2018-05-04 22:54:23 +02:00
|
|
|
Ok(Json(json!({
|
|
|
|
"UserId": user.uuid,
|
|
|
|
"PublicKey": user.public_key,
|
|
|
|
"Object":"userKey"
|
|
|
|
})))
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/keys", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_keys(data: JsonUpcase<KeysData>, headers: Headers, mut conn: DbConn) -> JsonResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: KeysData = data.into_inner().data;
|
2018-02-17 23:38:55 +01:00
|
|
|
|
2018-02-10 01:00:55 +01:00
|
|
|
let mut user = headers.user;
|
|
|
|
|
2018-06-01 00:35:30 +02:00
|
|
|
user.private_key = Some(data.EncryptedPrivateKey);
|
|
|
|
user.public_key = Some(data.PublicKey);
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
user.save(&mut conn).await?;
|
2020-05-08 19:38:49 +02:00
|
|
|
|
|
|
|
Ok(Json(json!({
|
|
|
|
"PrivateKey": user.private_key,
|
|
|
|
"PublicKey": user.public_key,
|
|
|
|
"Object":"keys"
|
|
|
|
})))
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2018-02-23 00:38:54 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct ChangePassData {
|
2018-06-01 00:18:50 +02:00
|
|
|
MasterPasswordHash: String,
|
|
|
|
NewMasterPasswordHash: String,
|
2022-10-17 17:23:21 +02:00
|
|
|
MasterPasswordHint: Option<String>,
|
2018-06-01 00:18:50 +02:00
|
|
|
Key: String,
|
2018-02-23 00:38:54 +01:00
|
|
|
}
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2018-02-23 00:38:54 +01:00
|
|
|
#[post("/accounts/password", data = "<data>")]
|
2022-11-20 19:15:45 +01:00
|
|
|
async fn post_password(
|
|
|
|
data: JsonUpcase<ChangePassData>,
|
|
|
|
headers: Headers,
|
|
|
|
mut conn: DbConn,
|
|
|
|
ip: ClientIp,
|
2022-12-30 21:23:55 +01:00
|
|
|
nt: Notify<'_>,
|
2022-11-20 19:15:45 +01:00
|
|
|
) -> EmptyResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: ChangePassData = data.into_inner().data;
|
2018-02-10 01:00:55 +01:00
|
|
|
let mut user = headers.user;
|
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
2018-02-10 01:00:55 +01:00
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2022-10-17 17:23:21 +02:00
|
|
|
user.password_hint = clean_password_hint(&data.MasterPasswordHint);
|
|
|
|
enforce_password_hint_setting(&user.password_hint)?;
|
|
|
|
|
2022-11-20 19:15:45 +01:00
|
|
|
log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;
|
|
|
|
|
Added web-vault v2.21.x support + some misc fixes
- The new web-vault v2.21.0+ has support for Master Password Reset. For
this to work it generates a public/private key-pair which needs to be
stored in the database. Currently the Master Password Reset is not
fixed, but there are endpoints which are needed even if we do not
support this feature (yet). This PR fixes those endpoints, and stores
the keys already in the database.
- There was an issue when you want to do a key-rotate when you change
your password, it also called an Emergency Access endpoint, which we do
not yet support. Because this endpoint failed to reply correctly
produced some errors, and also prevent the user from being forced to
logout. This resolves #1826 by adding at least that endpoint.
Because of that extra endpoint check to Emergency Access is done using
an old user stamp, i also modified the stamp exception to allow multiple
rocket routes to be called, and added an expiration timestamp to it.
During these tests i stumbled upon an issue that after my key-change was
done, it triggered the websockets to try and reload my ciphers, because
they were updated. This shouldn't happen when rotating they keys, since
all access should be invalided. Now there will be no websocket
notification for this, which also prevents error toasts.
- Increased Send Size limit to 500MB (with a litle overhead)
As a side note, i tested these changes on both v2.20.4 and v2.21.1 web-vault versions, all keeps working.
2021-07-04 23:02:56 +02:00
|
|
|
user.set_password(
|
|
|
|
&data.NewMasterPasswordHash,
|
2023-01-14 10:16:03 +01:00
|
|
|
Some(data.Key),
|
2023-01-24 13:06:31 +01:00
|
|
|
true,
|
2021-09-01 12:54:47 +02:00
|
|
|
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
|
Added web-vault v2.21.x support + some misc fixes
- The new web-vault v2.21.0+ has support for Master Password Reset. For
this to work it generates a public/private key-pair which needs to be
stored in the database. Currently the Master Password Reset is not
fixed, but there are endpoints which are needed even if we do not
support this feature (yet). This PR fixes those endpoints, and stores
the keys already in the database.
- There was an issue when you want to do a key-rotate when you change
your password, it also called an Emergency Access endpoint, which we do
not yet support. Because this endpoint failed to reply correctly
produced some errors, and also prevent the user from being forced to
logout. This resolves #1826 by adding at least that endpoint.
Because of that extra endpoint check to Emergency Access is done using
an old user stamp, i also modified the stamp exception to allow multiple
rocket routes to be called, and added an expiration timestamp to it.
During these tests i stumbled upon an issue that after my key-change was
done, it triggered the websockets to try and reload my ciphers, because
they were updated. This shouldn't happen when rotating they keys, since
all access should be invalided. Now there will be no websocket
notification for this, which also prevents error toasts.
- Increased Send Size limit to 500MB (with a litle overhead)
As a side note, i tested these changes on both v2.20.4 and v2.21.1 web-vault versions, all keeps working.
2021-07-04 23:02:56 +02:00
|
|
|
);
|
2023-01-14 10:16:03 +01:00
|
|
|
|
2022-12-30 21:23:55 +01:00
|
|
|
let save_result = user.save(&mut conn).await;
|
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
// Prevent loging out the client where the user requested this endpoint from.
|
|
|
|
// If you do logout the user it will causes issues at the client side.
|
|
|
|
// Adding the device uuid will prevent this.
|
|
|
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
2022-12-30 21:23:55 +01:00
|
|
|
|
|
|
|
save_result
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2018-09-19 17:30:14 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct ChangeKdfData {
|
|
|
|
Kdf: i32,
|
|
|
|
KdfIterations: i32,
|
|
|
|
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
NewMasterPasswordHash: String,
|
|
|
|
Key: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/kdf", data = "<data>")]
|
2022-12-30 21:23:55 +01:00
|
|
|
async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
2018-09-19 17:30:14 +02:00
|
|
|
let data: ChangeKdfData = data.into_inner().data;
|
|
|
|
let mut user = headers.user;
|
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2023-01-24 13:06:31 +01:00
|
|
|
if data.KdfIterations < 100_000 {
|
|
|
|
err!("KDF iterations lower then 100000 are not allowed.")
|
|
|
|
}
|
|
|
|
|
2018-09-19 17:30:14 +02:00
|
|
|
user.client_kdf_iter = data.KdfIterations;
|
|
|
|
user.client_kdf_type = data.Kdf;
|
2023-01-14 10:16:03 +01:00
|
|
|
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
2022-12-30 21:23:55 +01:00
|
|
|
let save_result = user.save(&mut conn).await;
|
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
2022-12-30 21:23:55 +01:00
|
|
|
|
|
|
|
save_result
|
2018-09-19 17:30:14 +02:00
|
|
|
}
|
|
|
|
|
2018-11-24 23:00:41 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct UpdateFolderData {
|
|
|
|
Id: String,
|
|
|
|
Name: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
use super::ciphers::CipherData;
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct KeyData {
|
|
|
|
Ciphers: Vec<CipherData>,
|
|
|
|
Folders: Vec<UpdateFolderData>,
|
|
|
|
Key: String,
|
|
|
|
PrivateKey: String,
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/key", data = "<data>")]
|
2022-11-20 19:15:45 +01:00
|
|
|
async fn post_rotatekey(
|
|
|
|
data: JsonUpcase<KeyData>,
|
|
|
|
headers: Headers,
|
|
|
|
mut conn: DbConn,
|
|
|
|
ip: ClientIp,
|
|
|
|
nt: Notify<'_>,
|
|
|
|
) -> EmptyResult {
|
2018-11-24 23:00:41 +01:00
|
|
|
let data: KeyData = data.into_inner().data;
|
|
|
|
|
|
|
|
if !headers.user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
// Validate the import before continuing
|
|
|
|
// Bitwarden does not process the import if there is one item invalid.
|
|
|
|
// Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
|
|
|
|
// TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
|
|
|
|
Cipher::validate_notes(&data.Ciphers)?;
|
|
|
|
|
2018-11-24 23:00:41 +01:00
|
|
|
let user_uuid = &headers.user.uuid;
|
|
|
|
|
|
|
|
// Update folder data
|
|
|
|
for folder_data in data.Folders {
|
2022-05-20 23:39:47 +02:00
|
|
|
let mut saved_folder = match Folder::find_by_uuid(&folder_data.Id, &mut conn).await {
|
2018-11-24 23:00:41 +01:00
|
|
|
Some(folder) => folder,
|
|
|
|
None => err!("Folder doesn't exist"),
|
|
|
|
};
|
|
|
|
|
|
|
|
if &saved_folder.user_uuid != user_uuid {
|
|
|
|
err!("The folder is not owned by the user")
|
|
|
|
}
|
|
|
|
|
|
|
|
saved_folder.name = folder_data.Name;
|
2022-05-20 23:39:47 +02:00
|
|
|
saved_folder.save(&mut conn).await?
|
2018-11-24 23:00:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update cipher data
|
|
|
|
use super::ciphers::update_cipher_from_data;
|
|
|
|
|
|
|
|
for cipher_data in data.Ciphers {
|
2022-05-20 23:39:47 +02:00
|
|
|
let mut saved_cipher = match Cipher::find_by_uuid(cipher_data.Id.as_ref().unwrap(), &mut conn).await {
|
2018-11-24 23:00:41 +01:00
|
|
|
Some(cipher) => cipher,
|
|
|
|
None => err!("Cipher doesn't exist"),
|
|
|
|
};
|
|
|
|
|
|
|
|
if saved_cipher.user_uuid.as_ref().unwrap() != user_uuid {
|
|
|
|
err!("The cipher is not owned by the user")
|
|
|
|
}
|
|
|
|
|
Added web-vault v2.21.x support + some misc fixes
- The new web-vault v2.21.0+ has support for Master Password Reset. For
this to work it generates a public/private key-pair which needs to be
stored in the database. Currently the Master Password Reset is not
fixed, but there are endpoints which are needed even if we do not
support this feature (yet). This PR fixes those endpoints, and stores
the keys already in the database.
- There was an issue when you want to do a key-rotate when you change
your password, it also called an Emergency Access endpoint, which we do
not yet support. Because this endpoint failed to reply correctly
produced some errors, and also prevent the user from being forced to
logout. This resolves #1826 by adding at least that endpoint.
Because of that extra endpoint check to Emergency Access is done using
an old user stamp, i also modified the stamp exception to allow multiple
rocket routes to be called, and added an expiration timestamp to it.
During these tests i stumbled upon an issue that after my key-change was
done, it triggered the websockets to try and reload my ciphers, because
they were updated. This shouldn't happen when rotating they keys, since
all access should be invalided. Now there will be no websocket
notification for this, which also prevents error toasts.
- Increased Send Size limit to 500MB (with a litle overhead)
As a side note, i tested these changes on both v2.20.4 and v2.21.1 web-vault versions, all keeps working.
2021-07-04 23:02:56 +02:00
|
|
|
// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
|
|
|
|
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
|
2022-12-30 21:23:55 +01:00
|
|
|
// We force the users to logout after the user has been saved to try and prevent these issues.
|
2022-11-20 19:15:45 +01:00
|
|
|
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None)
|
2022-05-20 23:39:47 +02:00
|
|
|
.await?
|
2018-11-24 23:00:41 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update user data
|
|
|
|
let mut user = headers.user;
|
|
|
|
|
2019-05-20 21:24:29 +02:00
|
|
|
user.akey = data.Key;
|
2018-11-24 23:00:41 +01:00
|
|
|
user.private_key = Some(data.PrivateKey);
|
|
|
|
user.reset_security_stamp();
|
|
|
|
|
2022-12-30 21:23:55 +01:00
|
|
|
let save_result = user.save(&mut conn).await;
|
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
// Prevent loging out the client where the user requested this endpoint from.
|
|
|
|
// If you do logout the user it will causes issues at the client side.
|
|
|
|
// Adding the device uuid will prevent this.
|
|
|
|
nt.send_logout(&user, Some(headers.device.uuid)).await;
|
2022-12-30 21:23:55 +01:00
|
|
|
|
|
|
|
save_result
|
2018-11-24 23:00:41 +01:00
|
|
|
}
|
|
|
|
|
2018-02-10 01:00:55 +01:00
|
|
|
#[post("/accounts/security-stamp", data = "<data>")]
|
2022-12-30 21:23:55 +01:00
|
|
|
async fn post_sstamp(
|
|
|
|
data: JsonUpcase<PasswordData>,
|
|
|
|
headers: Headers,
|
|
|
|
mut conn: DbConn,
|
|
|
|
nt: Notify<'_>,
|
|
|
|
) -> EmptyResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: PasswordData = data.into_inner().data;
|
2018-02-10 01:00:55 +01:00
|
|
|
let mut user = headers.user;
|
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
2018-02-10 01:00:55 +01:00
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
2018-02-10 01:00:55 +01:00
|
|
|
user.reset_security_stamp();
|
2022-12-30 21:23:55 +01:00
|
|
|
let save_result = user.save(&mut conn).await;
|
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
nt.send_logout(&user, None).await;
|
2022-12-30 21:23:55 +01:00
|
|
|
|
|
|
|
save_result
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2018-02-23 00:38:54 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
2018-06-17 00:06:59 +02:00
|
|
|
struct EmailTokenData {
|
2018-06-01 00:18:50 +02:00
|
|
|
MasterPasswordHash: String,
|
|
|
|
NewEmail: String,
|
2018-02-23 00:38:54 +01:00
|
|
|
}
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2018-02-23 00:38:54 +01:00
|
|
|
#[post("/accounts/email-token", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_email_token(data: JsonUpcase<EmailTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
2018-06-17 00:06:59 +02:00
|
|
|
let data: EmailTokenData = data.into_inner().data;
|
2019-11-25 06:28:49 +01:00
|
|
|
let mut user = headers.user;
|
2018-06-17 00:06:59 +02:00
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
2018-06-17 00:06:59 +02:00
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
2018-06-17 00:06:59 +02:00
|
|
|
err!("Email already in use");
|
|
|
|
}
|
|
|
|
|
2020-05-24 23:00:26 +02:00
|
|
|
if !CONFIG.is_email_domain_allowed(&data.NewEmail) {
|
|
|
|
err!("Email domain not allowed");
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
|
|
|
|
2022-01-24 10:17:00 +01:00
|
|
|
let token = crypto::generate_email_token(6);
|
2019-11-25 06:28:49 +01:00
|
|
|
|
|
|
|
if CONFIG.mail_enabled() {
|
2022-07-06 23:57:37 +02:00
|
|
|
if let Err(e) = mail::send_change_email(&data.NewEmail, &token).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
error!("Error sending change-email email: {:#?}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
user.email_new = Some(data.NewEmail);
|
|
|
|
user.email_new_token = Some(token);
|
2022-05-20 23:39:47 +02:00
|
|
|
user.save(&mut conn).await
|
2018-06-17 00:06:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct ChangeEmailData {
|
|
|
|
MasterPasswordHash: String,
|
|
|
|
NewEmail: String,
|
2018-11-24 23:00:41 +01:00
|
|
|
|
2018-06-17 00:06:59 +02:00
|
|
|
Key: String,
|
|
|
|
NewMasterPasswordHash: String,
|
2019-11-25 06:28:49 +01:00
|
|
|
Token: NumberOrString,
|
2018-06-17 00:06:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/email", data = "<data>")]
|
2022-12-30 21:23:55 +01:00
|
|
|
async fn post_email(
|
|
|
|
data: JsonUpcase<ChangeEmailData>,
|
|
|
|
headers: Headers,
|
|
|
|
mut conn: DbConn,
|
|
|
|
nt: Notify<'_>,
|
|
|
|
) -> EmptyResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: ChangeEmailData = data.into_inner().data;
|
2018-02-10 01:00:55 +01:00
|
|
|
let mut user = headers.user;
|
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
2018-02-10 01:00:55 +01:00
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
if User::find_by_mail(&data.NewEmail, &mut conn).await.is_some() {
|
2018-02-15 00:40:34 +01:00
|
|
|
err!("Email already in use");
|
|
|
|
}
|
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
match user.email_new {
|
|
|
|
Some(ref val) => {
|
2019-12-06 22:12:41 +01:00
|
|
|
if val != &data.NewEmail {
|
2019-11-25 06:28:49 +01:00
|
|
|
err!("Email change mismatch");
|
|
|
|
}
|
2019-12-06 22:12:41 +01:00
|
|
|
}
|
2019-11-25 06:28:49 +01:00
|
|
|
None => err!("No email change pending"),
|
|
|
|
}
|
|
|
|
|
|
|
|
if CONFIG.mail_enabled() {
|
|
|
|
// Only check the token if we sent out an email...
|
|
|
|
match user.email_new_token {
|
2019-12-06 22:12:41 +01:00
|
|
|
Some(ref val) => {
|
2019-11-25 06:28:49 +01:00
|
|
|
if *val != data.Token.into_string() {
|
|
|
|
err!("Token mismatch");
|
|
|
|
}
|
2019-12-06 22:12:41 +01:00
|
|
|
}
|
2019-11-25 06:28:49 +01:00
|
|
|
None => err!("No email change pending"),
|
|
|
|
}
|
|
|
|
user.verified_at = Some(Utc::now().naive_utc());
|
|
|
|
} else {
|
|
|
|
user.verified_at = None;
|
|
|
|
}
|
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
user.email = data.NewEmail;
|
2019-11-25 06:28:49 +01:00
|
|
|
user.email_new = None;
|
|
|
|
user.email_new_token = None;
|
2018-11-24 23:00:41 +01:00
|
|
|
|
2023-01-14 10:16:03 +01:00
|
|
|
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
|
|
|
|
2022-12-30 21:23:55 +01:00
|
|
|
let save_result = user.save(&mut conn).await;
|
2018-06-17 00:06:59 +02:00
|
|
|
|
2023-01-20 15:43:45 +01:00
|
|
|
nt.send_logout(&user, None).await;
|
2022-12-30 21:23:55 +01:00
|
|
|
|
|
|
|
save_result
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
2019-11-25 06:28:49 +01:00
|
|
|
#[post("/accounts/verify-email")]
|
2022-07-06 23:57:37 +02:00
|
|
|
async fn post_verify_email(headers: Headers) -> EmptyResult {
|
2019-11-25 06:28:49 +01:00
|
|
|
let user = headers.user;
|
|
|
|
|
|
|
|
if !CONFIG.mail_enabled() {
|
|
|
|
err!("Cannot verify email address");
|
|
|
|
}
|
|
|
|
|
2022-07-06 23:57:37 +02:00
|
|
|
if let Err(e) = mail::send_verify_email(&user.email, &user.uuid).await {
|
2020-10-23 10:30:25 +02:00
|
|
|
error!("Error sending verify_email email: {:#?}", e);
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct VerifyEmailTokenData {
|
|
|
|
UserId: String,
|
|
|
|
Token: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/verify-email-token", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_verify_email_token(data: JsonUpcase<VerifyEmailTokenData>, mut conn: DbConn) -> EmptyResult {
|
2019-11-25 06:28:49 +01:00
|
|
|
let data: VerifyEmailTokenData = data.into_inner().data;
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
let mut user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
Some(user) => user,
|
|
|
|
None => err!("User doesn't exist"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let claims = match decode_verify_email(&data.Token) {
|
|
|
|
Ok(claims) => claims,
|
|
|
|
Err(_) => err!("Invalid claim"),
|
|
|
|
};
|
|
|
|
if claims.sub != user.uuid {
|
2019-12-06 22:12:41 +01:00
|
|
|
err!("Invalid claim");
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
|
|
|
user.verified_at = Some(Utc::now().naive_utc());
|
|
|
|
user.last_verifying_at = None;
|
|
|
|
user.login_verify_count = 0;
|
2022-05-20 23:39:47 +02:00
|
|
|
if let Err(e) = user.save(&mut conn).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
error!("Error saving email verification: {:#?}", e);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct DeleteRecoverData {
|
|
|
|
Email: String,
|
|
|
|
}
|
|
|
|
|
2019-12-06 22:12:41 +01:00
|
|
|
#[post("/accounts/delete-recover", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_delete_recover(data: JsonUpcase<DeleteRecoverData>, mut conn: DbConn) -> EmptyResult {
|
2019-11-25 06:28:49 +01:00
|
|
|
let data: DeleteRecoverData = data.into_inner().data;
|
|
|
|
|
|
|
|
if CONFIG.mail_enabled() {
|
2022-05-20 23:39:47 +02:00
|
|
|
if let Some(user) = User::find_by_mail(&data.Email, &mut conn).await {
|
2022-07-06 23:57:37 +02:00
|
|
|
if let Err(e) = mail::send_delete_account(&user.email, &user.uuid).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
error!("Error sending delete account email: {:#?}", e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
// We don't support sending emails, but we shouldn't allow anybody
|
|
|
|
// to delete accounts without at least logging in... And if the user
|
|
|
|
// cannot remember their password then they will need to contact
|
|
|
|
// the administrator to delete it...
|
|
|
|
err!("Please contact the administrator to delete your account");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct DeleteRecoverTokenData {
|
|
|
|
UserId: String,
|
|
|
|
Token: String,
|
|
|
|
}
|
|
|
|
|
2019-12-06 22:12:41 +01:00
|
|
|
#[post("/accounts/delete-recover-token", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn post_delete_recover_token(data: JsonUpcase<DeleteRecoverTokenData>, mut conn: DbConn) -> EmptyResult {
|
2019-11-25 06:28:49 +01:00
|
|
|
let data: DeleteRecoverTokenData = data.into_inner().data;
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
let user = match User::find_by_uuid(&data.UserId, &mut conn).await {
|
2019-11-25 06:28:49 +01:00
|
|
|
Some(user) => user,
|
|
|
|
None => err!("User doesn't exist"),
|
|
|
|
};
|
|
|
|
|
|
|
|
let claims = match decode_delete(&data.Token) {
|
|
|
|
Ok(claims) => claims,
|
|
|
|
Err(_) => err!("Invalid claim"),
|
|
|
|
};
|
|
|
|
if claims.sub != user.uuid {
|
2019-12-06 22:12:41 +01:00
|
|
|
err!("Invalid claim");
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
2022-05-20 23:39:47 +02:00
|
|
|
user.delete(&mut conn).await
|
2019-11-25 06:28:49 +01:00
|
|
|
}
|
|
|
|
|
2018-02-10 01:00:55 +01:00
|
|
|
#[post("/accounts/delete", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn post_delete_account(data: JsonUpcase<PasswordData>, headers: Headers, conn: DbConn) -> EmptyResult {
|
|
|
|
delete_account(data, headers, conn).await
|
2018-09-18 12:13:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[delete("/accounts", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn delete_account(data: JsonUpcase<PasswordData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
|
2018-06-01 00:18:50 +02:00
|
|
|
let data: PasswordData = data.into_inner().data;
|
2018-02-15 00:53:11 +01:00
|
|
|
let user = headers.user;
|
2018-02-10 01:00:55 +01:00
|
|
|
|
2018-06-01 00:18:50 +02:00
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
2018-02-10 01:00:55 +01:00
|
|
|
err!("Invalid password")
|
|
|
|
}
|
2018-11-24 23:00:41 +01:00
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
user.delete(&mut conn).await
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[get("/accounts/revision-date")]
|
2023-01-07 19:41:28 +01:00
|
|
|
fn revision_date(headers: Headers) -> JsonResult {
|
2018-08-10 18:17:44 +02:00
|
|
|
let revision_date = headers.user.updated_at.timestamp_millis();
|
2023-01-07 19:41:28 +01:00
|
|
|
Ok(Json(json!(revision_date)))
|
2018-02-10 01:00:55 +01:00
|
|
|
}
|
2018-08-10 15:21:42 +02:00
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
|
|
|
struct PasswordHintData {
|
|
|
|
Email: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/password-hint", data = "<data>")]
|
2022-05-20 23:39:47 +02:00
|
|
|
async fn password_hint(data: JsonUpcase<PasswordHintData>, mut conn: DbConn) -> EmptyResult {
|
2021-07-10 10:21:27 +02:00
|
|
|
if !CONFIG.mail_enabled() && !CONFIG.show_password_hint() {
|
|
|
|
err!("This server is not configured to provide password hints.");
|
|
|
|
}
|
2018-08-10 15:21:42 +02:00
|
|
|
|
2021-07-10 10:21:27 +02:00
|
|
|
const NO_HINT: &str = "Sorry, you have no password hint...";
|
2018-08-15 08:32:19 +02:00
|
|
|
|
2021-07-10 10:21:27 +02:00
|
|
|
let data: PasswordHintData = data.into_inner().data;
|
|
|
|
let email = &data.Email;
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
match User::find_by_mail(email, &mut conn).await {
|
2021-07-10 10:21:27 +02:00
|
|
|
None => {
|
|
|
|
// To prevent user enumeration, act as if the user exists.
|
|
|
|
if CONFIG.mail_enabled() {
|
|
|
|
// There is still a timing side channel here in that the code
|
|
|
|
// paths that send mail take noticeably longer than ones that
|
|
|
|
// don't. Add a randomized sleep to mitigate this somewhat.
|
2022-03-20 18:51:24 +01:00
|
|
|
use rand::{rngs::SmallRng, Rng, SeedableRng};
|
|
|
|
let mut rng = SmallRng::from_entropy();
|
2021-07-10 10:21:27 +02:00
|
|
|
let delta: i32 = 100;
|
2022-03-20 18:51:24 +01:00
|
|
|
let sleep_ms = (1_000 + rng.gen_range(-delta..=delta)) as u64;
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(sleep_ms)).await;
|
2021-07-10 10:21:27 +02:00
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
err!(NO_HINT);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Some(user) => {
|
|
|
|
let hint: Option<String> = user.password_hint;
|
|
|
|
if CONFIG.mail_enabled() {
|
2022-07-06 23:57:37 +02:00
|
|
|
mail::send_password_hint(email, hint).await?;
|
2021-07-10 10:21:27 +02:00
|
|
|
Ok(())
|
|
|
|
} else if let Some(hint) = hint {
|
2022-12-29 14:11:52 +01:00
|
|
|
err!(format!("Your password hint is: {hint}"));
|
2021-07-10 10:21:27 +02:00
|
|
|
} else {
|
|
|
|
err!(NO_HINT);
|
|
|
|
}
|
2018-09-11 13:04:34 +02:00
|
|
|
}
|
2018-08-10 15:21:42 +02:00
|
|
|
}
|
|
|
|
}
|
2018-08-24 19:02:34 +02:00
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
2022-03-20 18:51:24 +01:00
|
|
|
pub struct PreloginData {
|
2018-08-24 19:02:34 +02:00
|
|
|
Email: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/prelogin", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn prelogin(data: JsonUpcase<PreloginData>, conn: DbConn) -> Json<Value> {
|
2022-03-20 18:51:24 +01:00
|
|
|
_prelogin(data, conn).await
|
|
|
|
}
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
pub async fn _prelogin(data: JsonUpcase<PreloginData>, mut conn: DbConn) -> Json<Value> {
|
2018-08-24 19:02:34 +02:00
|
|
|
let data: PreloginData = data.into_inner().data;
|
|
|
|
|
2022-05-20 23:39:47 +02:00
|
|
|
let (kdf_type, kdf_iter) = match User::find_by_mail(&data.Email, &mut conn).await {
|
2018-09-19 17:30:14 +02:00
|
|
|
Some(user) => (user.client_kdf_type, user.client_kdf_iter),
|
|
|
|
None => (User::CLIENT_KDF_TYPE_DEFAULT, User::CLIENT_KDF_ITER_DEFAULT),
|
2018-09-13 23:04:52 +02:00
|
|
|
};
|
|
|
|
|
2021-03-27 16:07:26 +01:00
|
|
|
Json(json!({
|
2018-09-13 23:04:52 +02:00
|
|
|
"Kdf": kdf_type,
|
|
|
|
"KdfIterations": kdf_iter
|
2021-03-27 16:07:26 +01:00
|
|
|
}))
|
2018-08-24 19:02:34 +02:00
|
|
|
}
|
2022-01-19 11:51:26 +01:00
|
|
|
|
|
|
|
// https://github.com/bitwarden/server/blob/master/src/Api/Models/Request/Accounts/SecretVerificationRequestModel.cs
|
2020-09-25 18:26:48 +02:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[allow(non_snake_case)]
|
2022-01-19 11:51:26 +01:00
|
|
|
struct SecretVerificationRequest {
|
2020-09-25 18:26:48 +02:00
|
|
|
MasterPasswordHash: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/verify-password", data = "<data>")]
|
2022-01-19 11:51:26 +01:00
|
|
|
fn verify_password(data: JsonUpcase<SecretVerificationRequest>, headers: Headers) -> EmptyResult {
|
|
|
|
let data: SecretVerificationRequest = data.into_inner().data;
|
2020-09-25 18:26:48 +02:00
|
|
|
let user = headers.user;
|
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2022-01-19 11:51:26 +01:00
|
|
|
|
2022-01-30 22:03:27 +01:00
|
|
|
async fn _api_key(
|
|
|
|
data: JsonUpcase<SecretVerificationRequest>,
|
|
|
|
rotate: bool,
|
|
|
|
headers: Headers,
|
2022-05-20 23:39:47 +02:00
|
|
|
mut conn: DbConn,
|
2022-01-30 22:03:27 +01:00
|
|
|
) -> JsonResult {
|
2022-01-19 11:51:26 +01:00
|
|
|
let data: SecretVerificationRequest = data.into_inner().data;
|
|
|
|
let mut user = headers.user;
|
|
|
|
|
|
|
|
if !user.check_valid_password(&data.MasterPasswordHash) {
|
|
|
|
err!("Invalid password")
|
|
|
|
}
|
|
|
|
|
|
|
|
if rotate || user.api_key.is_none() {
|
|
|
|
user.api_key = Some(crypto::generate_api_key());
|
2022-05-20 23:39:47 +02:00
|
|
|
user.save(&mut conn).await.expect("Error saving API key");
|
2022-01-19 11:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Json(json!({
|
|
|
|
"ApiKey": user.api_key,
|
|
|
|
"Object": "apiKey",
|
|
|
|
})))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/api-key", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
|
|
_api_key(data, false, headers, conn).await
|
2022-01-19 11:51:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#[post("/accounts/rotate-api-key", data = "<data>")]
|
2021-11-16 17:07:55 +01:00
|
|
|
async fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: Headers, conn: DbConn) -> JsonResult {
|
|
|
|
_api_key(data, true, headers, conn).await
|
2022-01-19 11:51:26 +01:00
|
|
|
}
|
2022-11-06 18:07:09 +01:00
|
|
|
|
|
|
|
#[get("/devices/knowndevice/<email>/<uuid>")]
|
2023-01-07 19:41:28 +01:00
|
|
|
async fn get_known_device(email: String, uuid: String, mut conn: DbConn) -> JsonResult {
|
2022-11-06 18:07:09 +01:00
|
|
|
// This endpoint doesn't have auth header
|
2023-01-07 19:41:28 +01:00
|
|
|
let mut result = false;
|
2022-11-06 18:07:09 +01:00
|
|
|
if let Some(user) = User::find_by_mail(&email, &mut conn).await {
|
2023-01-07 19:41:28 +01:00
|
|
|
result = Device::find_by_uuid_and_user(&uuid, &user.uuid, &mut conn).await.is_some();
|
2022-11-06 18:07:09 +01:00
|
|
|
}
|
2023-01-07 19:41:28 +01:00
|
|
|
Ok(Json(json!(result)))
|
2022-11-06 18:07:09 +01:00
|
|
|
}
|