Skip to content

Commit

Permalink
Add derive_pin_user_key (#508)
Browse files Browse the repository at this point in the history
Adds support for using pin when require master password is enabled.
  • Loading branch information
Hinton authored Jan 15, 2024
1 parent d2cb4f9 commit a1e57c1
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 22 deletions.
13 changes: 13 additions & 0 deletions crates/bitwarden-uniffi/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::sync::Arc;
use bitwarden::mobile::crypto::{
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
};
use bitwarden_crypto::EncString;

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

Expand Down Expand Up @@ -53,4 +54,16 @@ impl ClientCrypto {
pub async fn derive_pin_key(&self, pin: String) -> Result<DerivePinKeyResponse> {
Ok(self.0 .0.write().await.crypto().derive_pin_key(pin).await?)
}

/// Derives the pin protected user key from encrypted pin. Used when pin requires master password on first unlock.
pub async fn derive_pin_user_key(&self, encrypted_pin: EncString) -> Result<EncString> {
Ok(self
.0
.0
.write()
.await
.crypto()
.derive_pin_user_key(encrypted_pin)
.await?)
}
}
12 changes: 10 additions & 2 deletions crates/bitwarden/src/mobile/client_crypto.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#[cfg(feature = "internal")]
use bitwarden_crypto::EncString;

use crate::Client;
#[cfg(feature = "internal")]
use crate::{
error::Result,
mobile::crypto::{
derive_pin_key, get_user_encryption_key, initialize_org_crypto, initialize_user_crypto,
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
derive_pin_key, derive_pin_user_key, get_user_encryption_key, initialize_org_crypto,
initialize_user_crypto, DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest,
},
};

Expand Down Expand Up @@ -32,6 +35,11 @@ impl<'a> ClientCrypto<'a> {
pub async fn derive_pin_key(&mut self, pin: String) -> Result<DerivePinKeyResponse> {
derive_pin_key(self.client, pin)
}

#[cfg(feature = "internal")]
pub async fn derive_pin_user_key(&mut self, encrypted_pin: EncString) -> Result<EncString> {
derive_pin_user_key(self.client, encrypted_pin)
}
}

impl<'a> Client {
Expand Down
102 changes: 86 additions & 16 deletions crates/bitwarden/src/mobile/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
use std::collections::HashMap;

use bitwarden_crypto::{AsymmEncString, EncString};
#[cfg(feature = "internal")]
use bitwarden_crypto::{KeyDecryptable, KeyEncryptable, MasterKey, SymmetricCryptoKey};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[cfg(feature = "internal")]
use crate::client::{LoginMethod, UserLoginMethod};
use crate::{
client::Kdf,
error::{Error, Result},
Expand Down Expand Up @@ -50,8 +54,6 @@ pub enum InitUserCryptoMethod {

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

let login_method = crate::client::LoginMethod::User(crate::client::UserLoginMethod::Username {
client_id: "".to_string(),
email: req.email,
Expand Down Expand Up @@ -112,35 +114,65 @@ pub async fn get_user_encryption_key(client: &mut Client) -> Result<String> {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "mobile", derive(uniffi::Record))]
pub struct DerivePinKeyResponse {
/// [UserKey] protected by PIN
pin_protected_user_key: EncString,
/// PIN protected by [UserKey]
encrypted_pin: EncString,
}

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

use crate::client::{LoginMethod, UserLoginMethod};

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)?;

let login_method = client
.login_method
.as_ref()
.ok_or(Error::NotAuthenticated)?;

let pin_protected_user_key = derive_pin_protected_user_key(&pin, login_method, user_key)?;

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

#[cfg(feature = "internal")]
pub fn derive_pin_user_key(client: &mut Client, encrypted_pin: EncString) -> Result<EncString> {
let user_key = client
.get_encryption_settings()?
.get_key(&None)
.ok_or(Error::VaultLocked)?;

let pin: String = encrypted_pin.decrypt_with_key(user_key)?;
let login_method = client
.login_method
.as_ref()
.ok_or(Error::NotAuthenticated)?;

derive_pin_protected_user_key(&pin, login_method, user_key)
}

#[cfg(feature = "internal")]
fn derive_pin_protected_user_key(
pin: &str,
login_method: &LoginMethod,
user_key: &SymmetricCryptoKey,
) -> Result<EncString> {
let derived_key = match login_method {
LoginMethod::User(
UserLoginMethod::Username { email, kdf, .. }
| UserLoginMethod::ApiKey { email, kdf, .. },
) => MasterKey::derive(pin.as_bytes(), email.as_bytes(), kdf)?,
_ => return Err(Error::NotAuthenticated),
};

Ok(derived_key.encrypt_user_key(user_key)?)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -171,8 +203,8 @@ mod tests {

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

// Verify we can unlock with the pin
let mut client2 = Client::new(None);

initialize_user_crypto(
&mut client2,
InitUserCryptoRequest {
Expand Down Expand Up @@ -204,5 +236,43 @@ mod tests {
.unwrap()
.to_base64()
);

// Verify we can derive the pin protected user key from the encrypted pin
let pin_protected_user_key =
derive_pin_user_key(&mut client, pin_key.encrypted_pin).unwrap();

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

initialize_user_crypto(
&mut client3,
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,
},
},
)
.await
.unwrap();

assert_eq!(
client
.get_encryption_settings()
.unwrap()
.get_key(&None)
.unwrap()
.to_base64(),
client3
.get_encryption_settings()
.unwrap()
.get_key(&None)
.unwrap()
.to_base64()
);
}
}
35 changes: 31 additions & 4 deletions languages/kotlin/doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,18 @@ initialize another client instance by using the PIN and the PIN key with

**Output**: std::result::Result<DerivePinKeyResponse,BitwardenError>

### `derive_pin_user_key`

Derives the pin protected user key from encrypted pin. Used when pin requires master password on
first unlock.

**Arguments**:

- self:
- encrypted_pin: [EncString](#encstring)

**Output**: std::result::Result<EncString,BitwardenError>

## ClientExporters

### `export_vault`
Expand Down Expand Up @@ -852,6 +864,16 @@ implementations.
</tr>
</table>

## `EncString`

<table>
<tr>
<th>Key</th>
<th>Type</th>
<th>Description</th>
</tr>
</table>

## `ExportFormat`

<table>
Expand Down Expand Up @@ -1428,13 +1450,18 @@ implementations.
</tr>
<tr>
<th>key</th>
<th></th>
<th></th>
<th>string,null</th>
<th>Base64 encoded key</th>
</tr>
<tr>
<th>password</th>
<th>newPassword</th>
<th>string,null</th>
<th></th>
<th>Replace or add a password to an existing send. The SDK will always return None when decrypting a [Send] TODO: We should revisit this, one variant is to have &#x60;[Create, Update]SendView&#x60; DTOs.</th>
</tr>
<tr>
<th>hasPassword</th>
<th>boolean</th>
<th>Denote if an existing send has a password. The SDK will ignore this value when creating or updating sends.</th>
</tr>
<tr>
<th>type</th>
Expand Down

0 comments on commit a1e57c1

Please sign in to comment.