diff --git a/crates/bitwarden/src/auth/login/mod.rs b/crates/bitwarden/src/auth/login/mod.rs index b1ab54ecf..e3b3ebffa 100644 --- a/crates/bitwarden/src/auth/login/mod.rs +++ b/crates/bitwarden/src/auth/login/mod.rs @@ -47,7 +47,7 @@ async fn determine_password_hash( ) -> Result { let pre_login = request_prelogin(client, email.to_owned()).await?; let auth_settings = AuthSettings::new(pre_login, email.to_owned()); - let password_hash = auth_settings.make_user_password_hash(password)?; + let password_hash = auth_settings.derive_user_password_hash(password)?; client.set_auth_settings(auth_settings); Ok(password_hash) diff --git a/crates/bitwarden/src/client/auth_settings.rs b/crates/bitwarden/src/client/auth_settings.rs index 5e23947a1..8d9aac3ca 100644 --- a/crates/bitwarden/src/client/auth_settings.rs +++ b/crates/bitwarden/src/client/auth_settings.rs @@ -1,20 +1,21 @@ use std::num::NonZeroU32; -use base64::Engine; #[cfg(feature = "internal")] use bitwarden_api_identity::models::{KdfType, PreloginResponseModel}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +#[cfg(feature = "internal")] use crate::{ - crypto::{PbkdfSha256Hmac, PBKDF_SHA256_HMAC_OUT_SIZE}, + crypto::{HashPurpose, MasterKey}, error::Result, - util::BASE64_ENGINE, }; #[derive(Debug)] pub(crate) struct AuthSettings { + #[cfg(feature = "internal")] pub email: String, + #[cfg(feature = "internal")] pub(crate) kdf: Kdf, } @@ -66,73 +67,9 @@ impl AuthSettings { Self { email, kdf } } - pub fn make_user_password_hash(&self, password: &str) -> Result { - self.make_password_hash(password, &self.email) - } - - pub fn make_password_hash(&self, password: &str, salt: &str) -> Result { - let hash: [u8; 32] = - crate::crypto::hash_kdf(password.as_bytes(), salt.as_bytes(), &self.kdf)?; - - // Server expects hash + 1 iteration - let login_hash = pbkdf2::pbkdf2_array::( - &hash, - password.as_bytes(), - 1, - ) - .expect("hash is a valid fixed size"); - - Ok(BASE64_ENGINE.encode(login_hash)) - } -} - -#[cfg(test)] -mod tests { - use bitwarden_api_identity::models::{KdfType, PreloginResponseModel}; - - use super::AuthSettings; - - #[test] - fn test_password_hash_pbkdf2() { - let res = PreloginResponseModel { - kdf: Some(KdfType::Variant0), - kdf_iterations: Some(100_000), - kdf_memory: None, - kdf_parallelism: None, - }; - let settings = AuthSettings::new(res, "test@bitwarden.com".into()); - - assert_eq!( - settings - .make_password_hash("asdfasdf", "test_salt") - .unwrap(), - "ZF6HjxUTSyBHsC+HXSOhZoXN+UuMnygV5YkWXCY4VmM=" - ); - assert_eq!( - settings.make_user_password_hash("asdfasdf").unwrap(), - "wmyadRMyBZOH7P/a/ucTCbSghKgdzDpPqUnu/DAVtSw=" - ); - } - - #[test] - fn test_password_hash_argon2id() { - let res = PreloginResponseModel { - kdf: Some(KdfType::Variant1), - kdf_iterations: Some(4), - kdf_memory: Some(32), - kdf_parallelism: Some(2), - }; - let settings = AuthSettings::new(res, "test@bitwarden.com".into()); - - assert_eq!( - settings - .make_password_hash("asdfasdf", "test_salt") - .unwrap(), - "PR6UjYmjmppTYcdyTiNbAhPJuQQOmynKbdEl1oyi/iQ=" - ); - assert_eq!( - settings.make_user_password_hash("asdfasdf").unwrap(), - "ImYMPyd/X7FPrWzbt+wRfmlICWTA25yZrOob4TBMEZw=" - ); + #[cfg(feature = "internal")] + pub fn derive_user_password_hash(&self, password: &str) -> Result { + let master_key = MasterKey::derive(password.as_bytes(), self.email.as_bytes(), &self.kdf)?; + master_key.derive_master_key_hash(password.as_bytes(), HashPurpose::ServerAuthorization) } } diff --git a/crates/bitwarden/src/client/encryption_settings.rs b/crates/bitwarden/src/client/encryption_settings.rs index 29fd38b42..495884c3a 100644 --- a/crates/bitwarden/src/client/encryption_settings.rs +++ b/crates/bitwarden/src/client/encryption_settings.rs @@ -33,25 +33,13 @@ impl EncryptionSettings { user_key: EncString, private_key: EncString, ) -> Result { - use crate::crypto::decrypt_aes256; - - // Stretch keys from the provided password - let (key, mac_key) = crate::crypto::stretch_key_password( - password.as_bytes(), - auth.email.as_bytes(), - &auth.kdf, - )?; - - // Decrypt the user key with the stretched key - let user_key = { - let (iv, mac, data) = match user_key { - EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => (iv, mac, data), - _ => return Err(CryptoError::InvalidKey.into()), - }; + use crate::crypto::MasterKey; - let dec = decrypt_aes256(&iv, &mac, data, Some(mac_key), key)?; - SymmetricCryptoKey::try_from(dec.as_slice())? - }; + // Derive master key from password + let master_key = MasterKey::derive(password.as_bytes(), auth.email.as_bytes(), &auth.kdf)?; + + // Decrypt the user key + let user_key = master_key.decrypt_user_key(user_key)?; // Decrypt the private key with the user key let private_key = { diff --git a/crates/bitwarden/src/client/mod.rs b/crates/bitwarden/src/client/mod.rs index c7bd1918c..017d14ec8 100644 --- a/crates/bitwarden/src/client/mod.rs +++ b/crates/bitwarden/src/client/mod.rs @@ -2,7 +2,6 @@ pub(crate) use client::*; pub(crate) mod access_token; -#[cfg(any(feature = "internal", feature = "mobile"))] pub mod auth_settings; #[allow(clippy::module_inception)] mod client; diff --git a/crates/bitwarden/src/crypto/aes_ops.rs b/crates/bitwarden/src/crypto/aes_ops.rs index 109deac4f..eb5b82a60 100644 --- a/crates/bitwarden/src/crypto/aes_ops.rs +++ b/crates/bitwarden/src/crypto/aes_ops.rs @@ -10,24 +10,7 @@ use crate::{ error::{CryptoError, Result}, }; -pub fn decrypt_aes256( - iv: &[u8; 16], - mac: &[u8; 32], - data: Vec, - mac_key: Option>, - key: GenericArray, -) -> Result> { - let mac_key = match mac_key { - Some(k) => k, - None => return Err(CryptoError::InvalidMac.into()), - }; - - // Validate HMAC - let res = validate_mac(&mac_key, iv, &data)?; - if res != *mac { - return Err(CryptoError::InvalidMac.into()); - } - +pub fn decrypt_aes256(iv: &[u8; 16], data: Vec, key: GenericArray) -> Result> { // Decrypt data let iv = GenericArray::from_slice(iv); let mut data = data; @@ -42,6 +25,20 @@ pub fn decrypt_aes256( Ok(data) } +pub fn decrypt_aes256_hmac( + iv: &[u8; 16], + mac: &[u8; 32], + data: Vec, + mac_key: GenericArray, + key: GenericArray, +) -> Result> { + let res = validate_mac(&mac_key, iv, &data)?; + if res != *mac { + return Err(CryptoError::InvalidMac.into()); + } + decrypt_aes256(iv, data, key) +} + pub fn encrypt_aes256( data_dec: &[u8], mac_key: Option>, diff --git a/crates/bitwarden/src/crypto/enc_string.rs b/crates/bitwarden/src/crypto/enc_string.rs index 571806846..7b739ffe9 100644 --- a/crates/bitwarden/src/crypto/enc_string.rs +++ b/crates/bitwarden/src/crypto/enc_string.rs @@ -6,7 +6,7 @@ use uuid::Uuid; use crate::{ client::encryption_settings::EncryptionSettings, - crypto::{decrypt_aes256, Decryptable, Encryptable, SymmetricCryptoKey}, + crypto::{decrypt_aes256_hmac, Decryptable, Encryptable, SymmetricCryptoKey}, error::{CryptoError, EncStringParseError, Error, Result}, util::BASE64_ENGINE, }; @@ -184,48 +184,22 @@ impl EncString { impl Display for EncString { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}.", self.enc_type())?; - - let mut parts = Vec::<&[u8]>::new(); - - match self { - EncString::AesCbc256_B64 { iv, data } => { - parts.push(iv); - parts.push(data); - } - EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } => { - parts.push(iv); - parts.push(data); - parts.push(mac); - } - EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { - parts.push(iv); - parts.push(data); - parts.push(mac); - } - EncString::Rsa2048_OaepSha256_B64 { data } => { - parts.push(data); - } - EncString::Rsa2048_OaepSha1_B64 { data } => { - parts.push(data); - } - EncString::Rsa2048_OaepSha256_HmacSha256_B64 { mac, data } => { - parts.push(data); - parts.push(mac); - } - EncString::Rsa2048_OaepSha1_HmacSha256_B64 { mac, data } => { - parts.push(data); - parts.push(mac); - } - } - - for i in 0..parts.len() { - if i == parts.len() - 1 { - write!(f, "{}", BASE64_ENGINE.encode(parts[i]))?; - } else { - write!(f, "{}|", BASE64_ENGINE.encode(parts[i]))?; - } - } + let parts: Vec<&[u8]> = match self { + EncString::AesCbc256_B64 { iv, data } => vec![iv, data], + EncString::AesCbc128_HmacSha256_B64 { iv, mac, data } => vec![iv, data, mac], + EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => vec![iv, data, mac], + EncString::Rsa2048_OaepSha256_B64 { data } => vec![data], + EncString::Rsa2048_OaepSha1_B64 { data } => vec![data], + EncString::Rsa2048_OaepSha256_HmacSha256_B64 { mac, data } => vec![data, mac], + EncString::Rsa2048_OaepSha1_HmacSha256_B64 { mac, data } => vec![data, mac], + }; + + let encoded_parts: Vec = parts + .iter() + .map(|part| BASE64_ENGINE.encode(part)) + .collect(); + + write!(f, "{}.{}", self.enc_type(), encoded_parts.join("|"))?; Ok(()) } @@ -241,7 +215,7 @@ impl<'de> Deserialize<'de> for EncString { type Value = EncString; fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "A valid string") + write!(f, "a valid string") } fn visit_str(self, v: &str) -> Result @@ -281,7 +255,8 @@ impl EncString { pub fn decrypt_with_key(&self, key: &SymmetricCryptoKey) -> Result> { match self { EncString::AesCbc256_HmacSha256_B64 { iv, mac, data } => { - let dec = decrypt_aes256(iv, mac, data.clone(), key.mac_key, key.key)?; + let mac_key = key.mac_key.ok_or(CryptoError::InvalidMac)?; + let dec = decrypt_aes256_hmac(iv, mac, data.clone(), mac_key, key.key)?; Ok(dec) } _ => Err(CryptoError::InvalidKey.into()), diff --git a/crates/bitwarden/src/crypto/master_key.rs b/crates/bitwarden/src/crypto/master_key.rs index f2df690f9..dee95d662 100644 --- a/crates/bitwarden/src/crypto/master_key.rs +++ b/crates/bitwarden/src/crypto/master_key.rs @@ -1,17 +1,58 @@ use aes::cipher::typenum::U32; +use base64::Engine; -use super::hkdf_expand; +use crate::util::BASE64_ENGINE; + +use super::{ + hkdf_expand, EncString, PbkdfSha256Hmac, SymmetricCryptoKey, PBKDF_SHA256_HMAC_OUT_SIZE, +}; use { crate::{client::auth_settings::Kdf, error::Result}, aes::cipher::generic_array::GenericArray, sha2::Digest, }; -pub(crate) fn hash_kdf(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result<[u8; 32]> { - use crate::crypto::PBKDF_SHA256_HMAC_OUT_SIZE; +#[derive(Copy, Clone)] +pub(crate) enum HashPurpose { + ServerAuthorization = 1, + // LocalAuthorization = 2, +} + +/// A Master Key. +pub(crate) struct MasterKey(SymmetricCryptoKey); + +impl MasterKey { + /// Derives a users master key from their password, email and KDF. + pub fn derive(password: &[u8], email: &[u8], kdf: &Kdf) -> Result { + derive_key(password, email, kdf).map(Self) + } + + /// Derive the master key hash, used for server authorization. + pub(crate) fn derive_master_key_hash( + &self, + password: &[u8], + purpose: HashPurpose, + ) -> Result { + let hash = pbkdf2::pbkdf2_array::( + &self.0.key, + password, + purpose as u32, + ) + .expect("hash is a valid fixed size"); + + Ok(BASE64_ENGINE.encode(hash)) + } - use super::PbkdfSha256Hmac; + pub(crate) fn decrypt_user_key(&self, user_key: EncString) -> Result { + let stretched_key = stretch_master_key(self)?; + let dec = user_key.decrypt_with_key(&stretched_key)?; + SymmetricCryptoKey::try_from(dec.as_slice()) + } +} + +/// Derive a generic key from a secret and salt using the provided KDF. +fn derive_key(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result { let hash = match kdf { Kdf::PBKDF2 { iterations } => pbkdf2::pbkdf2_array::< PbkdfSha256Hmac, @@ -47,32 +88,29 @@ pub(crate) fn hash_kdf(secret: &[u8], salt: &[u8], kdf: &Kdf) -> Result<[u8; 32] hash } }; - Ok(hash) + SymmetricCryptoKey::try_from(hash.as_slice()) } -pub(crate) fn stretch_key_password( - secret: &[u8], - salt: &[u8], - kdf: &Kdf, -) -> Result<(GenericArray, GenericArray)> { - let master_key: [u8; 32] = hash_kdf(secret, salt, kdf)?; - - let key: GenericArray = hkdf_expand(&master_key, Some("enc"))?; - let mac_key: GenericArray = hkdf_expand(&master_key, Some("mac"))?; +fn stretch_master_key(master_key: &MasterKey) -> Result { + let key: GenericArray = hkdf_expand(&master_key.0.key, Some("enc"))?; + let mac_key: GenericArray = hkdf_expand(&master_key.0.key, Some("mac"))?; - Ok((key, mac_key)) + Ok(SymmetricCryptoKey { + key, + mac_key: Some(mac_key), + }) } #[cfg(test)] mod tests { - use { - crate::{client::auth_settings::Kdf, crypto::stretch_key_password}, - std::num::NonZeroU32, - }; + use crate::crypto::SymmetricCryptoKey; + + use super::{stretch_master_key, HashPurpose, MasterKey}; + use {crate::client::auth_settings::Kdf, std::num::NonZeroU32}; #[test] - fn test_key_stretch_password_pbkdf2() { - let (key, mac) = stretch_key_password( + fn test_master_key_derive_pbkdf2() { + let master_key = MasterKey::derive( &b"67t9b5g67$%Dh89n"[..], "test_key".as_bytes(), &Kdf::PBKDF2 { @@ -82,24 +120,18 @@ mod tests { .unwrap(); assert_eq!( - key.as_slice(), [ - 111, 31, 178, 45, 238, 152, 37, 114, 143, 215, 124, 83, 135, 173, 195, 23, 142, - 134, 120, 249, 61, 132, 163, 182, 113, 197, 189, 204, 188, 21, 237, 96 - ] - ); - assert_eq!( - mac.as_slice(), - [ - 221, 127, 206, 234, 101, 27, 202, 38, 86, 52, 34, 28, 78, 28, 185, 16, 48, 61, 127, - 166, 209, 247, 194, 87, 232, 26, 48, 85, 193, 249, 179, 155 - ] + 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, + 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75 + ], + master_key.0.key.as_slice() ); + assert_eq!(None, master_key.0.mac_key); } #[test] - fn test_key_stretch_password_argon2() { - let (key, mac) = stretch_key_password( + fn test_master_key_derive_argon2() { + let master_key = MasterKey::derive( &b"67t9b5g67$%Dh89n"[..], "test_key".as_bytes(), &Kdf::Argon2id { @@ -111,18 +143,79 @@ mod tests { .unwrap(); assert_eq!( - key.as_slice(), [ - 236, 253, 166, 121, 207, 124, 98, 149, 42, 141, 97, 226, 207, 71, 173, 60, 10, 0, - 184, 255, 252, 87, 62, 32, 188, 166, 173, 223, 146, 159, 222, 219 + 207, 240, 225, 177, 162, 19, 163, 76, 98, 106, 179, 175, 224, 9, 17, 240, 20, 147, + 237, 47, 246, 150, 141, 184, 62, 225, 131, 242, 51, 53, 225, 242 + ], + master_key.0.key.as_slice() + ); + assert_eq!(None, master_key.0.mac_key); + } + + #[test] + fn test_stretch_master_key() { + let master_key = MasterKey(SymmetricCryptoKey { + key: [ + 31, 79, 104, 226, 150, 71, 177, 90, 194, 80, 172, 209, 17, 129, 132, 81, 138, 167, + 69, 167, 254, 149, 2, 27, 39, 197, 64, 42, 22, 195, 86, 75, ] + .into(), + mac_key: None, + }); + + let stretched = stretch_master_key(&master_key).unwrap(); + + assert_eq!( + [ + 111, 31, 178, 45, 238, 152, 37, 114, 143, 215, 124, 83, 135, 173, 195, 23, 142, + 134, 120, 249, 61, 132, 163, 182, 113, 197, 189, 204, 188, 21, 237, 96 + ], + stretched.key.as_slice() ); assert_eq!( - mac.as_slice(), [ - 214, 144, 76, 173, 225, 106, 132, 131, 173, 56, 134, 241, 223, 227, 165, 161, 146, - 37, 111, 206, 155, 24, 224, 151, 134, 189, 202, 0, 27, 149, 131, 21 - ] + 221, 127, 206, 234, 101, 27, 202, 38, 86, 52, 34, 28, 78, 28, 185, 16, 48, 61, 127, + 166, 209, 247, 194, 87, 232, 26, 48, 85, 193, 249, 179, 155 + ], + stretched.mac_key.unwrap().as_slice() + ); + } + + #[test] + fn test_password_hash_pbkdf2() { + let password = "asdfasdf".as_bytes(); + let salt = "test_salt".as_bytes(); + let kdf = Kdf::PBKDF2 { + iterations: NonZeroU32::new(100_000).unwrap(), + }; + + let master_key = MasterKey::derive(password, salt, &kdf).unwrap(); + + assert_eq!( + "ZF6HjxUTSyBHsC+HXSOhZoXN+UuMnygV5YkWXCY4VmM=", + master_key + .derive_master_key_hash(password, HashPurpose::ServerAuthorization) + .unwrap(), + ); + } + + #[test] + fn test_password_hash_argon2id() { + let password = "asdfasdf".as_bytes(); + let salt = "test_salt".as_bytes(); + let kdf = Kdf::Argon2id { + iterations: NonZeroU32::new(4).unwrap(), + memory: NonZeroU32::new(32).unwrap(), + parallelism: NonZeroU32::new(2).unwrap(), + }; + + let master_key = MasterKey::derive(password, salt, &kdf).unwrap(); + + assert_eq!( + "PR6UjYmjmppTYcdyTiNbAhPJuQQOmynKbdEl1oyi/iQ=", + master_key + .derive_master_key_hash(password, HashPurpose::ServerAuthorization) + .unwrap(), ); } } diff --git a/crates/bitwarden/src/crypto/mod.rs b/crates/bitwarden/src/crypto/mod.rs index 2482387fb..93d4eb1a1 100644 --- a/crates/bitwarden/src/crypto/mod.rs +++ b/crates/bitwarden/src/crypto/mod.rs @@ -16,6 +16,8 @@ //! //! - `CryptoService.makeSendKey` & `AccessService.createAccessToken` are replaced by the generic //! `derive_shareable_key` +//! - MasterKey operations such as `makeMasterKey` and `hashMasterKey` are moved to the MasterKey +//! struct. //! use aes::cipher::{generic_array::GenericArray, ArrayLength, Unsigned}; @@ -28,7 +30,7 @@ pub use enc_string::EncString; mod encryptable; pub use encryptable::{Decryptable, Encryptable}; mod aes_ops; -pub use aes_ops::{decrypt_aes256, encrypt_aes256}; +pub use aes_ops::{decrypt_aes256, decrypt_aes256_hmac, encrypt_aes256}; mod symmetric_crypto_key; pub use symmetric_crypto_key::SymmetricCryptoKey; mod shareable_key; @@ -37,7 +39,7 @@ pub(crate) use shareable_key::derive_shareable_key; #[cfg(feature = "internal")] mod master_key; #[cfg(feature = "internal")] -pub(crate) use master_key::{hash_kdf, stretch_key_password}; +pub(crate) use master_key::{HashPurpose, MasterKey}; #[cfg(feature = "internal")] mod fingerprint; diff --git a/crates/bitwarden/src/crypto/symmetric_crypto_key.rs b/crates/bitwarden/src/crypto/symmetric_crypto_key.rs index 4d0f573d9..7b2086f75 100644 --- a/crates/bitwarden/src/crypto/symmetric_crypto_key.rs +++ b/crates/bitwarden/src/crypto/symmetric_crypto_key.rs @@ -9,6 +9,7 @@ use crate::{ util::BASE64_ENGINE, }; +/// A symmetric encryption key. Used to encrypt and decrypt [`EncString`](crate::crypto::EncString) pub struct SymmetricCryptoKey { pub key: GenericArray, pub mac_key: Option>, diff --git a/crates/bitwarden/src/mobile/kdf.rs b/crates/bitwarden/src/mobile/kdf.rs index bebf73423..e79e0ab12 100644 --- a/crates/bitwarden/src/mobile/kdf.rs +++ b/crates/bitwarden/src/mobile/kdf.rs @@ -14,6 +14,6 @@ pub async fn hash_password( email, kdf: kdf_params, }; - let hash = auth_settings.make_user_password_hash(&password)?; + let hash = auth_settings.derive_user_password_hash(&password)?; Ok(hash) } diff --git a/crates/bitwarden/src/platform/get_user_api_key.rs b/crates/bitwarden/src/platform/get_user_api_key.rs index 98035ad45..629bfc8c4 100644 --- a/crates/bitwarden/src/platform/get_user_api_key.rs +++ b/crates/bitwarden/src/platform/get_user_api_key.rs @@ -48,7 +48,7 @@ fn get_secret_verification_request( let master_password_hash = input .master_password .as_ref() - .map(|p| auth_settings.make_user_password_hash(p)) + .map(|p| auth_settings.derive_user_password_hash(p)) .transpose()?; Ok(SecretVerificationRequestModel { master_password_hash,