use std::sync::Arc; use anyhow::Result; use password_hash::SaltString; use rocket::{request::FromRequest, serde::json::Json, Request, State}; use serde::{Deserialize, Serialize}; use validator::Validate; use crate::{ api::auth, auth::{AuthSource, Authenticated}, crypto::{ random_bytes, AccountResetReq, AccountResetToken, AuthPW, KeyBundle, KeyFetchReq, KeyFetchToken, PasswordChangeReq, PasswordChangeToken, }, db::{Db, DbConn}, mailer::Mailer, types::{HawkKey, OauthToken, PasswordChangeID, SecretKey, UserID, VerifyHash}, }; // MISSING get /password/forgot/status // MISSING post /password/create // MISSING post /password/forgot/resend_code #[derive(Debug, Deserialize, Validate)] #[serde(deny_unknown_fields)] #[allow(non_snake_case)] pub(crate) struct ChangeStartReq { #[validate(email, length(min = 3, max = 256))] email: String, oldAuthPW: AuthPW, } #[derive(Debug, Serialize)] #[allow(non_snake_case)] pub(crate) struct ChangeStartResp { keyFetchToken: KeyFetchToken, passwordChangeToken: PasswordChangeToken, } #[post("/password/change/start", data = "")] pub(crate) async fn change_start( db: &DbConn, data: Json, ) -> auth::Result { let data = data.into_inner(); data.validate().map_err(|_| auth::Error::InvalidParameter)?; let (uid, user) = db.get_user(&data.email).await.map_err(|_| auth::Error::UnknownAccount)?; if user.email != data.email { return Err(auth::Error::IncorrectEmailCase); } if !user.verified { return Err(auth::Error::UnverifiedAccount); } let stretched = data.oldAuthPW.stretch(user.auth_salt.as_salt())?; if stretched.verify_hash() != user.verify_hash.0 { return Err(auth::Error::IncorrectPassword); } let change_token = PasswordChangeToken::generate(); let change_req = PasswordChangeReq::derive_from_change_token(&change_token); let key_fetch_token = KeyFetchToken::generate(); let key_req = KeyFetchReq::derive_from(&key_fetch_token); let wrapped = key_req .derive_resp() .wrap_keys(&KeyBundle { ka: user.ka, wrap_kb: stretched.decrypt_wwkb(&user.wrapwrap_kb) }); db.add_key_fetch(key_req.token_id, &key_req.req_hmac_key, &wrapped).await?; db.add_password_change(&uid, &change_req.token_id, &change_req.req_hmac_key, None).await?; Ok(Json(ChangeStartResp { keyFetchToken: key_fetch_token, passwordChangeToken: change_token })) } // NOTE we use a plain bool here and in the db instead of an enum because // enums aren't usable in const generics in stable. #[derive(Debug)] pub(crate) struct WithChangeToken; #[async_trait] impl AuthSource for WithChangeToken { type ID = PasswordChangeID; type Context = (UserID, Option); async fn hawk(r: &Request<'_>, id: &PasswordChangeID) -> Result<(HawkKey, Self::Context)> { // unlike key fetch we'll use a separate transaction here since the body of the // handler can fail. let pool = <&Db as FromRequest>::from_request(r) .await .success_or_else(|| anyhow!("could not open db connection"))?; let db = pool.begin().await?; let result = db.finish_password_change(id, IS_FORGOT).await?; db.commit().await?; Ok(result) } async fn bearer_token( _: &Request<'_>, _: &OauthToken, ) -> Result<(PasswordChangeID, Self::Context)> { bail!("invalid password change authentication") } } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] #[allow(non_snake_case)] pub(crate) struct ChangeFinishReq { authPW: AuthPW, wrapKb: SecretKey, // MISSING sessionToken } #[derive(Debug, Serialize)] #[allow(non_snake_case)] pub(crate) struct ChangeFinishResp { // NOTE we intentionally deviate from mozilla here. mozilla creates a new // session if sessionToken is set in the request, but we use the "legacy" // password change mechanism that leaves the requesting session and its // device and keys intact. as such this struct is intentionally empty. // // MISSING uid // MISSING sessionToken // MISSING verified // MISSING authAt // MISSING keyFetchToken } #[post("/password/change/finish", data = "")] pub(crate) async fn change_finish( db: &DbConn, mailer: &State>, data: Authenticated>, ) -> auth::Result { let user = db.get_user_by_id(&data.context.0).await?; let auth_salt = SaltString::generate(rand::rngs::OsRng); let stretched = data.body.authPW.stretch(auth_salt.as_salt())?; let verify_hash = stretched.verify_hash(); let wrapwrap_kb = stretched.rewrap_wkb(&data.body.wrapKb); db.change_user_auth( &data.context.0, auth_salt, SecretKey(wrapwrap_kb.0), VerifyHash(verify_hash), ) .await?; // NOTE password_changed/password_reset pushes seem to have no effect, so skip them. mailer .send_password_changed(&user.email) .await .map_err(|e| { warn!("password change email send failed: {e}"); }) .ok(); Ok(Json(ChangeFinishResp {})) } #[derive(Debug, Deserialize, Validate)] #[serde(deny_unknown_fields)] #[allow(non_snake_case)] pub(crate) struct ForgotStartReq { #[validate(email, length(min = 3, max = 256))] email: String, } #[derive(Debug, Serialize)] #[allow(non_snake_case)] pub(crate) struct ForgotStartResp { passwordForgotToken: PasswordChangeToken, ttl: u32, codeLength: u32, tries: u32, } #[post("/password/forgot/send_code", data = "")] pub(crate) async fn forgot_start( db: &DbConn, mailer: &State>, data: Json, ) -> auth::Result { let data = data.into_inner(); data.validate().map_err(|_| auth::Error::InvalidParameter)?; let (uid, user) = db.get_user(&data.email).await.map_err(|_| auth::Error::UnknownAccount)?; if user.email != data.email { return Err(auth::Error::IncorrectEmailCase); } if !user.verified { return Err(auth::Error::UnverifiedAccount); } let forgot_code = hex::encode(random_bytes::<16>()); let forgot_token = PasswordChangeToken::generate(); let forgot_req = PasswordChangeReq::derive_from_forgot_token(&forgot_token); db.add_password_change( &uid, &forgot_req.token_id, &forgot_req.req_hmac_key, Some(&forgot_code), ) .await?; mailer.send_password_forgot(&user.email, &forgot_code).await?; Ok(Json(ForgotStartResp { passwordForgotToken: forgot_token, ttl: 300, codeLength: 16, tries: 1, })) } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] #[allow(non_snake_case)] pub(crate) struct ForgotFinishReq { code: String, // MISSING accountResetWithRecoveryKey } #[derive(Debug, Serialize)] #[allow(non_snake_case)] pub(crate) struct ForgotFinishResp { accountResetToken: AccountResetToken, } #[post("/password/forgot/verify_code", data = "")] pub(crate) async fn forgot_finish( db: &DbConn, data: Authenticated>, ) -> auth::Result { if Some(data.body.code) != data.context.1 { return Err(auth::Error::InvalidVerificationCode); } let reset_token = AccountResetToken::generate(); let reset_req = AccountResetReq::derive_from(&reset_token); db.add_account_reset(&data.context.0, &reset_req.token_id, &reset_req.req_hmac_key).await?; Ok(Json(ForgotFinishResp { accountResetToken: reset_token })) }