Skip to content

Commit

Permalink
[PM-4694] Implement PIN unlock (#421)
Browse files Browse the repository at this point in the history
## Type of change
```
- [ ] Bug fix
- [x] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
```

## Objective
Implement support for deriving the PIN encrypted user key and
initializing crypto with it. Updated the mobile app demos to use this
new functionality.
  • Loading branch information
dani-garcia authored Dec 12, 2023
1 parent afb1f78 commit 826d272
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 20 deletions.
10 changes: 9 additions & 1 deletion crates/bitwarden-uniffi/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::sync::Arc;

use bitwarden::mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest};
use bitwarden::mobile::crypto::{
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
};

use crate::{error::Result, Client};

Expand Down Expand Up @@ -45,4 +47,10 @@ impl ClientCrypto {
.get_user_encryption_key()
.await?)
}

/// Generates a PIN protected user key from the provided PIN. The result can be stored and later used
/// to initialize another client instance by using the PIN and the PIN key with `initialize_user_crypto`.
pub async fn derive_pin_key(&self, pin: String) -> Result<DerivePinKeyResponse> {
Ok(self.0 .0.write().await.crypto().derive_pin_key(pin).await?)
}
}
24 changes: 22 additions & 2 deletions crates/bitwarden/src/client/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,10 +224,9 @@ impl Client {
#[cfg(feature = "mobile")]
pub(crate) fn initialize_user_crypto_decrypted_key(
&mut self,
decrypted_user_key: &str,
user_key: SymmetricCryptoKey,
private_key: EncString,
) -> Result<&EncryptionSettings> {
let user_key = decrypted_user_key.parse::<SymmetricCryptoKey>()?;
self.encryption_settings = Some(EncryptionSettings::new_decrypted_key(
user_key,
private_key,
Expand All @@ -238,6 +237,27 @@ impl Client {
.expect("It was initialized on the previous line"))
}

#[cfg(feature = "mobile")]
pub(crate) fn initialize_user_crypto_pin(
&mut self,
pin: &str,
pin_protected_user_key: EncString,
private_key: EncString,
) -> Result<&EncryptionSettings> {
use crate::crypto::MasterKey;

let pin_key = match &self.login_method {
Some(LoginMethod::User(
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. },
)) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?,
_ => return Err(Error::NotAuthenticated),
};

let decrypted_user_key = pin_key.decrypt_user_key(pin_protected_user_key)?;
self.initialize_user_crypto_decrypted_key(decrypted_user_key, private_key)
}

pub(crate) fn initialize_crypto_single_key(
&mut self,
key: SymmetricCryptoKey,
Expand Down
41 changes: 32 additions & 9 deletions crates/bitwarden/src/crypto/master_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ impl MasterKey {
let dec: Vec<u8> = user_key.decrypt_with_key(&stretched_key)?;
SymmetricCryptoKey::try_from(dec.as_slice())
}

pub(crate) fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result<EncString> {
let stretched_key = stretch_master_key(self)?;

EncString::encrypt_aes256_hmac(
user_key.to_vec().as_slice(),
stretched_key.mac_key.unwrap(),
stretched_key.key,
)
}
}

/// Generate a new random user key and encrypt it with the master key.
Expand All @@ -62,15 +72,9 @@ fn make_user_key(
let mut user_key = [0u8; 64];
rng.fill(&mut user_key);

let stretched_key = stretch_master_key(master_key)?;
let protected = EncString::encrypt_aes256_hmac(
&user_key,
stretched_key.mac_key.unwrap(),
stretched_key.key,
)?;

let u: &[u8] = &user_key;
Ok((UserKey::new(SymmetricCryptoKey::try_from(u)?), protected))
let user_key = SymmetricCryptoKey::try_from(user_key.as_slice())?;
let protected = master_key.encrypt_user_key(&user_key)?;
Ok((UserKey::new(user_key), protected))
}

/// Derive a generic key from a secret and salt using the provided KDF.
Expand Down Expand Up @@ -285,4 +289,23 @@ mod tests {
"Decrypted key doesn't match user key"
);
}

#[test]
fn test_make_user_key2() {
let master_key = MasterKey(SymmetricCryptoKey::generate("test1"));

let user_key = SymmetricCryptoKey::generate("test2");

let encrypted = master_key.encrypt_user_key(&user_key).unwrap();
let decrypted = master_key.decrypt_user_key(encrypted).unwrap();

assert_eq!(
decrypted.key, user_key.key,
"Decrypted key doesn't match user key"
);
assert_eq!(
decrypted.mac_key, user_key.mac_key,
"Decrypted key doesn't match user key"
);
}
}
10 changes: 10 additions & 0 deletions crates/bitwarden/src/crypto/symmetric_crypto_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ impl SymmetricCryptoKey {

BASE64_ENGINE.encode(&buf)
}

#[cfg(feature = "internal")]
pub(super) fn to_vec(&self) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&self.key);
if let Some(mac) = self.mac_key {
buf.extend_from_slice(&mac);
}
buf
}
}

impl FromStr for SymmetricCryptoKey {
Expand Down
9 changes: 7 additions & 2 deletions crates/bitwarden/src/mobile/client_crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use crate::Client;
use crate::{
error::Result,
mobile::crypto::{
get_user_encryption_key, initialize_org_crypto, initialize_user_crypto,
InitOrgCryptoRequest, InitUserCryptoRequest,
derive_pin_key, get_user_encryption_key, initialize_org_crypto, initialize_user_crypto,
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
},
};

Expand All @@ -27,6 +27,11 @@ impl<'a> ClientCrypto<'a> {
pub async fn get_user_encryption_key(&mut self) -> Result<String> {
get_user_encryption_key(self.client).await
}

#[cfg(feature = "internal")]
pub async fn derive_pin_key(&mut self, pin: String) -> Result<DerivePinKeyResponse> {
derive_pin_key(self.client, pin)
}
}

impl<'a> Client {
Expand Down
118 changes: 117 additions & 1 deletion crates/bitwarden/src/mobile/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,18 @@ pub enum InitUserCryptoMethod {
/// The user's decrypted encryption key, obtained using `get_user_encryption_key`
decrypted_user_key: String,
},
Pin {
/// The user's PIN
pin: String,
/// The user's symmetric crypto key, encrypted with the PIN. Use `derive_pin_key` to obtain this.
pin_protected_user_key: EncString,
},
}

#[cfg(feature = "internal")]
pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequest) -> Result<()> {
use crate::crypto::SymmetricCryptoKey;

let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username {
client_id: "".to_string(),
email: req.email,
Expand All @@ -59,7 +67,14 @@ pub async fn initialize_user_crypto(client: &mut Client, req: InitUserCryptoRequ
client.initialize_user_crypto(&password, user_key, private_key)?;
}
InitUserCryptoMethod::DecryptedKey { decrypted_user_key } => {
client.initialize_user_crypto_decrypted_key(&decrypted_user_key, private_key)?;
let user_key = decrypted_user_key.parse::<SymmetricCryptoKey>()?;
client.initialize_user_crypto_decrypted_key(user_key, private_key)?;
}
InitUserCryptoMethod::Pin {
pin,
pin_protected_user_key,
} => {
client.initialize_user_crypto_pin(&pin, pin_protected_user_key, private_key)?;
}
}

Expand Down Expand Up @@ -91,3 +106,104 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result<String> {

Ok(user_key.to_base64())
}

#[cfg(feature = "internal")]
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "mobile", derive(uniffi::Record))]
pub struct DerivePinKeyResponse {
pin_protected_user_key: EncString,
encrypted_pin: EncString,
}

#[cfg(feature = "internal")]
pub fn derive_pin_key(client: &mut Client, pin: String) -> Result<DerivePinKeyResponse> {
use crate::{
client::{LoginMethod, UserLoginMethod},
crypto::{KeyEncryptable, MasterKey},
};

let derived_key = match &client.login_method {
Some(LoginMethod::User(
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. },
)) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?,
_ => return Err(Error::NotAuthenticated),
};

let user_key = client
.get_encryption_settings()?
.get_key(&None)
.ok_or(Error::VaultLocked)?;

Ok(DerivePinKeyResponse {
pin_protected_user_key: derived_key.encrypt_user_key(user_key)?,
encrypted_pin: pin.encrypt_with_key(user_key)?,
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{client::kdf::Kdf, Client};

#[tokio::test]
async fn test_initialize_user_crypto_pin() {
let mut client = Client::new(None);

let priv_key = "2.kmLY8NJVuiKBFJtNd/ZFpA==|qOodlRXER+9ogCe3yOibRHmUcSNvjSKhdDuztLlucs10jLiNoVVVAc+9KfNErLSpx5wmUF1hBOJM8zwVPjgQTrmnNf/wuDpwiaCxNYb/0v4FygPy7ccAHK94xP1lfqq7U9+tv+/yiZSwgcT+xF0wFpoxQeNdNRFzPTuD9o4134n8bzacD9DV/WjcrXfRjbBCzzuUGj1e78+A7BWN7/5IWLz87KWk8G7O/W4+8PtEzlwkru6Wd1xO19GYU18oArCWCNoegSmcGn7w7NDEXlwD403oY8Oa7ylnbqGE28PVJx+HLPNIdSC6YKXeIOMnVs7Mctd/wXC93zGxAWD6ooTCzHSPVV50zKJmWIG2cVVUS7j35H3rGDtUHLI+ASXMEux9REZB8CdVOZMzp2wYeiOpggebJy6MKOZqPT1R3X0fqF2dHtRFPXrNsVr1Qt6bS9qTyO4ag1/BCvXF3P1uJEsI812BFAne3cYHy5bIOxuozPfipJrTb5WH35bxhElqwT3y/o/6JWOGg3HLDun31YmiZ2HScAsUAcEkA4hhoTNnqy4O2s3yVbCcR7jF7NLsbQc0MDTbnjxTdI4VnqUIn8s2c9hIJy/j80pmO9Bjxp+LQ9a2hUkfHgFhgHxZUVaeGVth8zG2kkgGdrp5VHhxMVFfvB26Ka6q6qE/UcS2lONSv+4T8niVRJz57qwctj8MNOkA3PTEfe/DP/LKMefke31YfT0xogHsLhDkx+mS8FCc01HReTjKLktk/Jh9mXwC5oKwueWWwlxI935ecn+3I2kAuOfMsgPLkoEBlwgiREC1pM7VVX1x8WmzIQVQTHd4iwnX96QewYckGRfNYWz/zwvWnjWlfcg8kRSe+68EHOGeRtC5r27fWLqRc0HNcjwpgHkI/b6czerCe8+07TWql4keJxJxhBYj3iOH7r9ZS8ck51XnOb8tGL1isimAJXodYGzakwktqHAD7MZhS+P02O+6jrg7d+yPC2ZCuS/3TOplYOCHQIhnZtR87PXTUwr83zfOwAwCyv6KP84JUQ45+DItrXLap7nOVZKQ5QxYIlbThAO6eima6Zu5XHfqGPMNWv0bLf5+vAjIa5np5DJrSwz9no/hj6CUh0iyI+SJq4RGI60lKtypMvF6MR3nHLEHOycRUQbZIyTHWl4QQLdHzuwN9lv10ouTEvNr6sFflAX2yb6w3hlCo7oBytH3rJekjb3IIOzBpeTPIejxzVlh0N9OT5MZdh4sNKYHUoWJ8mnfjdM+L4j5Q2Kgk/XiGDgEebkUxiEOQUdVpePF5uSCE+TPav/9FIRGXGiFn6NJMaU7aBsDTFBLloffFLYDpd8/bTwoSvifkj7buwLYM+h/qcnfdy5FWau1cKav+Blq/ZC0qBpo658RTC8ZtseAFDgXoQZuksM10hpP9bzD04Bx30xTGX81QbaSTNwSEEVrOtIhbDrj9OI43KH4O6zLzK+t30QxAv5zjk10RZ4+5SAdYndIlld9Y62opCfPDzRy3ubdve4ZEchpIKWTQvIxq3T5ogOhGaWBVYnkMtM2GVqvWV//46gET5SH/MdcwhACUcZ9kCpMnWH9CyyUwYvTT3UlNyV+DlS27LMPvaw7tx7qa+GfNCoCBd8S4esZpQYK/WReiS8=|pc7qpD42wxyXemdNPuwxbh8iIaryrBPu8f/DGwYdHTw=";

initialize_user_crypto(
&mut client,
InitUserCryptoRequest {
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: priv_key.to_owned(),
method: InitUserCryptoMethod::Password {
password: "asdfasdfasdf".into(),
user_key: "2.u2HDQ/nH2J7f5tYHctZx6Q==|NnUKODz8TPycWJA5svexe1wJIz2VexvLbZh2RDfhj5VI3wP8ZkR0Vicvdv7oJRyLI1GyaZDBCf9CTBunRTYUk39DbZl42Rb+Xmzds02EQhc=|rwuo5wgqvTJf3rgwOUfabUyzqhguMYb3sGBjOYqjevc=".into(),
},
},
)
.await
.unwrap();

let pin_key = derive_pin_key(&mut client, "1234".into()).unwrap();

let mut client2 = Client::new(None);

initialize_user_crypto(
&mut client2,
InitUserCryptoRequest {
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: priv_key.to_owned(),
method: InitUserCryptoMethod::Pin {
pin: "1234".into(),
pin_protected_user_key: pin_key.pin_protected_user_key,
},
},
)
.await
.unwrap();

assert_eq!(
client
.get_encryption_settings()
.unwrap()
.get_key(&None)
.unwrap()
.to_base64(),
client2
.get_encryption_settings()
.unwrap()
.get_key(&None)
.unwrap()
.to_base64()
);
}
}
Loading

0 comments on commit 826d272

Please sign in to comment.