Skip to content

Commit

Permalink
[PM-4930] Implement password validation (#401)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hinton authored Dec 7, 2023
1 parent 6602110 commit 557fb14
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 68 deletions.
20 changes: 19 additions & 1 deletion crates/bitwarden-uniffi/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;
use bitwarden::{
auth::{password::MasterPasswordPolicyOptions, RegisterKeyResponse},
client::kdf::Kdf,
crypto::HashPurpose,
};

use crate::{error::Result, Client};
Expand Down Expand Up @@ -50,14 +51,15 @@ impl ClientAuth {
email: String,
password: String,
kdf_params: Kdf,
purpose: HashPurpose,
) -> Result<String> {
Ok(self
.0
.0
.read()
.await
.kdf()
.hash_password(email, password, kdf_params)
.hash_password(email, password, kdf_params, purpose)
.await?)
}

Expand All @@ -76,4 +78,20 @@ impl ClientAuth {
.auth()
.make_register_keys(email, password, kdf)?)
}

/// Validate the user password
///
/// To retrieve the user's password hash, use [`ClientAuth::hash_password`] with
/// `HashPurpose::LocalAuthentication` during login and persist it. If the login method has no
/// password, use the email OTP.
pub async fn validate_password(&self, password: String, password_hash: String) -> Result<bool> {
Ok(self
.0
.0
.write()
.await
.auth()
.validate_password(password, password_hash.to_string())
.await?)
}
}
2 changes: 2 additions & 0 deletions crates/bitwarden-uniffi/src/docs.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bitwarden::{
auth::password::MasterPasswordPolicyOptions,
client::kdf::Kdf,
crypto::HashPurpose,
mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest},
platform::FingerprintRequest,
tool::{ExportFormat, PassphraseGeneratorRequest, PasswordGeneratorRequest},
Expand All @@ -27,6 +28,7 @@ pub enum DocRef {
// Crypto
InitUserCryptoRequest(InitUserCryptoRequest),
InitOrgCryptoRequest(InitOrgCryptoRequest),
HashPurpose(HashPurpose),

// Generators
PasswordGeneratorRequest(PasswordGeneratorRequest),
Expand Down
8 changes: 7 additions & 1 deletion crates/bitwarden/src/auth/client_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use crate::{
ApiKeyLoginResponse, PasswordLoginRequest, PasswordLoginResponse,
TwoFactorEmailRequest,
},
password::{password_strength, satisfies_policy, MasterPasswordPolicyOptions},
password::{
password_strength, satisfies_policy, validate_password, MasterPasswordPolicyOptions,
},
register::{make_register_keys, register},
RegisterKeyResponse, RegisterRequest,
},
Expand Down Expand Up @@ -90,6 +92,10 @@ impl<'a> ClientAuth<'a> {
pub async fn send_two_factor_email(&mut self, tf: &TwoFactorEmailRequest) -> Result<()> {
send_two_factor_email(self.client, tf).await
}

pub async fn validate_password(&self, password: String, password_hash: String) -> Result<bool> {
validate_password(self.client, password, password_hash).await
}
}

impl<'a> Client {
Expand Down
13 changes: 1 addition & 12 deletions crates/bitwarden/src/auth/login/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
#[cfg(feature = "internal")]
use {
crate::{
client::{kdf::Kdf, Client},
error::Result,
},
crate::{client::Client, error::Result},
bitwarden_api_identity::{
apis::accounts_api::accounts_prelogin_post,
models::{PreloginRequestModel, PreloginResponseModel},
Expand Down Expand Up @@ -39,14 +36,6 @@ pub(super) use access_token::login_access_token;
#[cfg(feature = "secrets")]
pub use access_token::{AccessTokenLoginRequest, AccessTokenLoginResponse};

#[cfg(feature = "internal")]
async fn determine_password_hash(email: &str, kdf: &Kdf, password: &str) -> Result<String> {
use crate::crypto::{HashPurpose, MasterKey};

let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?;
master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization)
}

#[cfg(feature = "internal")]
pub(crate) async fn request_prelogin(
client: &mut Client,
Expand Down
15 changes: 9 additions & 6 deletions crates/bitwarden/src/auth/login/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ use serde::{Deserialize, Serialize};

#[cfg(feature = "internal")]
use crate::{
auth::{
api::request::PasswordTokenRequest,
login::{determine_password_hash, TwoFactorRequest},
},
auth::{api::request::PasswordTokenRequest, login::TwoFactorRequest},
client::{kdf::Kdf, LoginMethod},
crypto::EncString,
Client,
Expand All @@ -26,12 +23,18 @@ pub(crate) async fn login_password(
client: &mut Client,
input: &PasswordLoginRequest,
) -> Result<PasswordLoginResponse> {
use crate::client::UserLoginMethod;
use crate::{auth::determine_password_hash, client::UserLoginMethod, crypto::HashPurpose};

info!("password logging in");
debug!("{:#?}, {:#?}", client, input);

let password_hash = determine_password_hash(&input.email, &input.kdf, &input.password).await?;
let password_hash = determine_password_hash(
&input.email,
&input.kdf,
&input.password,
HashPurpose::ServerAuthorization,
)
.await?;
let response = request_identity_tokens(client, input, &password_hash).await?;

if let IdentityTokenResponse::Authenticated(r) = &response {
Expand Down
11 changes: 8 additions & 3 deletions crates/bitwarden/src/auth/login/two_factor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};

use super::determine_password_hash;
use crate::{error::Result, Client};
use crate::{auth::determine_password_hash, crypto::HashPurpose, error::Result, Client};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand All @@ -22,7 +21,13 @@ pub(crate) async fn send_two_factor_email(
// TODO: This should be resolved from the client
let kdf = client.auth().prelogin(input.email.clone()).await?;

let password_hash = determine_password_hash(&input.email, &kdf, &input.password).await?;
let password_hash = determine_password_hash(
&input.email,
&kdf,
&input.password,
HashPurpose::ServerAuthorization,
)
.await?;

let config = client.get_api_configurations().await;
bitwarden_api_api::apis::two_factor_api::two_factor_send_email_login_post(
Expand Down
44 changes: 44 additions & 0 deletions crates/bitwarden/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,47 @@ pub use jwt_token::JWTToken;
mod register;
#[cfg(feature = "internal")]
pub use register::{RegisterKeyResponse, RegisterRequest};

#[cfg(feature = "internal")]
use crate::{
client::kdf::Kdf,
crypto::{HashPurpose, MasterKey},
error::Result,
};

#[cfg(feature = "internal")]
async fn determine_password_hash(
email: &str,
kdf: &Kdf,
password: &str,
purpose: HashPurpose,
) -> Result<String> {
let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), kdf)?;
master_key.derive_master_key_hash(password.as_bytes(), purpose)
}

#[cfg(test)]
mod tests {
use std::num::NonZeroU32;

use super::*;

#[cfg(feature = "internal")]
#[tokio::test]
async fn test_determine_password_hash() {
use super::determine_password_hash;

let password = "password123";
let email = "[email protected]";
let kdf = Kdf::PBKDF2 {
iterations: NonZeroU32::new(100_000).unwrap(),
};
let purpose = HashPurpose::LocalAuthorization;

let result = determine_password_hash(email, &kdf, password, purpose)
.await
.unwrap();

assert_eq!(result, "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU=");
}
}
65 changes: 65 additions & 0 deletions crates/bitwarden/src/auth/password.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use schemars::JsonSchema;

use super::determine_password_hash;
use crate::{
client::{LoginMethod, UserLoginMethod},
crypto::HashPurpose,
error::{Error, Result},
Client,
};

pub(super) fn password_strength(
_password: String,
_email: String,
Expand All @@ -16,6 +24,33 @@ pub(super) fn satisfies_policy(
true
}

/// Validate if the provided password matches the password hash stored in the client.
pub(super) async fn validate_password(
client: &Client,
password: String,
password_hash: String,
) -> Result<bool> {
let login_method = client
.login_method
.as_ref()
.ok_or(Error::NotAuthenticated)?;

if let LoginMethod::User(login_method) = login_method {
match login_method {
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. } => {
let hash =
determine_password_hash(email, kdf, &password, HashPurpose::LocalAuthorization)
.await?;

Ok(hash == password_hash)
}
}
} else {
Err(Error::NotAuthenticated)
}
}

#[derive(Debug, JsonSchema)]
#[cfg_attr(feature = "mobile", derive(uniffi::Record))]
#[allow(dead_code)]
Expand All @@ -32,3 +67,33 @@ pub struct MasterPasswordPolicyOptions {
/// the user will be forced to update their password.
enforce_on_login: bool,
}

#[cfg(test)]

mod tests {

#[cfg(feature = "mobile")]
#[tokio::test]
async fn test_validate_password() {
use std::num::NonZeroU32;

use super::validate_password;
use crate::client::{kdf::Kdf, Client, LoginMethod, UserLoginMethod};

let mut client = Client::new(None);
client.set_login_method(LoginMethod::User(UserLoginMethod::Username {
email: "[email protected]".to_string(),
kdf: Kdf::PBKDF2 {
iterations: NonZeroU32::new(100_000).unwrap(),
},
client_id: "1".to_string(),
}));

let password = "password123".to_string();
let password_hash = "7kTqkF1pY/3JeOu73N9kR99fDDe9O1JOZaVc7KH3lsU=".to_string();

let result = validate_password(&client, password, password_hash).await;

assert!(result.unwrap());
}
}
8 changes: 5 additions & 3 deletions crates/bitwarden/src/crypto/master_key.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use aes::cipher::{generic_array::GenericArray, typenum::U32};
use base64::Engine;
use rand::Rng;
use schemars::JsonSchema;
use sha2::Digest;

use super::{
Expand All @@ -9,10 +10,11 @@ use super::{
};
use crate::{client::kdf::Kdf, error::Result, util::BASE64_ENGINE};

#[derive(Copy, Clone)]
pub(crate) enum HashPurpose {
#[derive(Copy, Clone, JsonSchema)]
#[cfg_attr(feature = "mobile", derive(uniffi::Enum))]
pub enum HashPurpose {
ServerAuthorization = 1,
// LocalAuthorization = 2,
LocalAuthorization = 2,
}

/// A Master Key.
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden/src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ pub(crate) use shareable_key::derive_shareable_key;
#[cfg(feature = "internal")]
mod master_key;
#[cfg(feature = "internal")]
pub(crate) use master_key::{HashPurpose, MasterKey};
pub use master_key::HashPurpose;
#[cfg(feature = "internal")]
pub(crate) use master_key::MasterKey;
#[cfg(feature = "internal")]
mod user_key;
#[cfg(feature = "internal")]
Expand Down
7 changes: 5 additions & 2 deletions crates/bitwarden/src/mobile/client_kdf.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{client::kdf::Kdf, error::Result, mobile::kdf::hash_password, Client};
use crate::{
client::kdf::Kdf, crypto::HashPurpose, error::Result, mobile::kdf::hash_password, Client,
};

pub struct ClientKdf<'a> {
pub(crate) client: &'a crate::Client,
Expand All @@ -10,8 +12,9 @@ impl<'a> ClientKdf<'a> {
email: String,
password: String,
kdf_params: Kdf,
purpose: HashPurpose,
) -> Result<String> {
hash_password(self.client, email, password, kdf_params).await
hash_password(self.client, email, password, kdf_params, purpose).await
}
}

Expand Down
3 changes: 2 additions & 1 deletion crates/bitwarden/src/mobile/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ pub async fn hash_password(
email: String,
password: String,
kdf_params: Kdf,
purpose: HashPurpose,
) -> Result<String> {
let master_key = MasterKey::derive(password.as_bytes(), email.as_bytes(), &kdf_params)?;

master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization)
master_key.derive_master_key_hash(password.as_bytes(), purpose)
}
Loading

0 comments on commit 557fb14

Please sign in to comment.