Skip to content

[PM-24263] Pin protected key envelope unlock #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: km/beeep/safe-password-protected-key-envelope
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/bitwarden-core/src/client/encryption_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub enum EncryptionSettingsError {

#[error(transparent)]
UserIdAlreadySetError(#[from] UserIdAlreadySetError),

#[error("Wrong Pin")]
WrongPin,
}

#[allow(clippy::large_enum_variant)]
Expand Down
28 changes: 27 additions & 1 deletion crates/bitwarden-core/src/client/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ use crate::{
login_method::UserLoginMethod,
},
error::NotAuthenticatedError,
key_management::{crypto::InitUserCryptoRequest, SecurityState, SignedSecurityState},
key_management::{
crypto::InitUserCryptoRequest, PasswordProtectedKeyEnvelope, SecurityState,
SignedSecurityState,
},
};

/// Represents the user's keys, that are encrypted by the user key, and the signed security state.
Expand Down Expand Up @@ -309,6 +312,29 @@ impl InternalClient {
self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_state)
}

#[cfg(feature = "internal")]
pub(crate) fn initialize_user_crypto_pin_envelope(
&self,
pin: String,
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
key_state: UserKeyState,
) -> Result<(), EncryptionSettingsError> {
use crate::key_management::SymmetricKeyId;
let ctx = &mut self.key_store.context_mut();
let decrypted_user_key_id = pin_protected_user_key_envelope
.unseal(SymmetricKeyId::Local("tmp_unlock_pin"), &pin, ctx)
.map_err(|_| EncryptionSettingsError::WrongPin)?;

// Allowing deprecated here, until a refactor to pass the Local key ids to
// `initialized_user_crypto_decrypted_key`
#[allow(deprecated)]
let decrypted_user_key = ctx
.dangerous_get_symmetric_key(decrypted_user_key_id)?
.clone();

self.initialize_user_crypto_decrypted_key(decrypted_user_key, key_state)
}

#[cfg(feature = "secrets")]
pub(crate) fn initialize_crypto_single_org_key(
&self,
Expand Down
115 changes: 112 additions & 3 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{
dangerous_get_v2_rotated_account_keys, AsymmetricCryptoKey, CoseSerializable, CryptoError,
EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes,
SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey, UserKey,
PrimitiveEncryptable, SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes,
SymmetricCryptoKey, UnsignedSharedKey, UserKey,
};
use bitwarden_error::bitwarden_error;
use schemars::JsonSchema;
Expand All @@ -23,7 +23,8 @@ use crate::{
client::{encryption_settings::EncryptionSettingsError, LoginMethod, UserLoginMethod},
error::StatefulCryptoError,
key_management::{
AsymmetricKeyId, SecurityState, SignedSecurityState, SigningKeyId, SymmetricKeyId,
non_generic_wrappers::PasswordProtectedKeyEnvelope, AsymmetricKeyId, SecurityState,
SignedSecurityState, SigningKeyId, SymmetricKeyId,
},
Client, NotAuthenticatedError, VaultLockedError, WrongPasswordError,
};
Expand Down Expand Up @@ -68,6 +69,7 @@ pub struct InitUserCryptoRequest {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
#[allow(clippy::large_enum_variant)]
pub enum InitUserCryptoMethod {
/// Password
Password {
Expand All @@ -89,6 +91,13 @@ pub enum InitUserCryptoMethod {
/// this.
pin_protected_user_key: EncString,
},
/// PIN Envelope
PinEnvelope {
/// The user's PIN
pin: String,
/// The user's symmetric crypto key, encrypted with the PIN-protected key envelope.
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
},
/// Auth request
AuthRequest {
/// Private Key generated by the `crate::auth::new_auth_request`.
Expand Down Expand Up @@ -173,6 +182,16 @@ pub(super) async fn initialize_user_crypto(
key_state,
)?;
}
InitUserCryptoMethod::PinEnvelope {
pin,
pin_protected_user_key_envelope,
} => {
client.internal.initialize_user_crypto_pin_envelope(
pin,
pin_protected_user_key_envelope,
key_state,
)?;
}
InitUserCryptoMethod::AuthRequest {
request_private_key,
method,
Expand Down Expand Up @@ -315,6 +334,40 @@ pub(super) fn update_password(
})
}

/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct EnrollPinResponse {
/// [UserKey] protected by PIN
pin_protected_user_key_envelope: PasswordProtectedKeyEnvelope,
/// PIN protected by [UserKey]
user_key_encrypted_pin: EncString,
}

pub(super) fn enroll_pin(
client: &Client,
pin: String,
) -> Result<EnrollPinResponse, CryptoClientError> {
let key_store = client.internal.get_key_store();
let mut ctx = key_store.context_mut();

let key_envelope = PasswordProtectedKeyEnvelope(
bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::seal(
SymmetricKeyId::User,
&pin,
&ctx,
)
.map_err(CryptoError::PasswordProtectedKeyEnvelopeError)?,
);
let encrypted_pin = pin.encrypt(&mut ctx, SymmetricKeyId::User)?;
Ok(EnrollPinResponse {
pin_protected_user_key_envelope: key_envelope,
user_key_encrypted_pin: encrypted_pin,
})
}

/// Request for deriving a pin protected user key
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
Expand Down Expand Up @@ -937,6 +990,62 @@ mod tests {
assert_eq!(client_key, client3_key);
}

#[tokio::test]
async fn test_initialize_user_crypto_pin_envelope() {
let user_key = "5yKAZ4TSSEGje54MV5lc5ty6crkqUz4xvl+8Dm/piNLKf6OgRi2H0uzttNTXl9z6ILhkmuIXzGpAVc2YdorHgQ==";
let test_pin = "1234";

let client1 = Client::new(None);
initialize_user_crypto(
&client1,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
kdf_params: Kdf::PBKDF2 {
iterations: 100_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: make_key_pair(user_key.to_string())
.unwrap()
.user_key_encrypted_private_key,
signing_key: None,
security_state: None,
method: InitUserCryptoMethod::DecryptedKey {
decrypted_user_key: user_key.to_string(),
},
},
)
.await
.unwrap();

let enroll_response = client1.crypto().enroll_pin(test_pin.to_string()).unwrap();

let client1 = Client::new(None);
initialize_user_crypto(
&client1,
InitUserCryptoRequest {
user_id: Some(uuid::Uuid::new_v4()),
// NOTE: THIS CHANGES KDF SETTINGS. We ensure in this test that even with different
// KDF settings the pin can unlock the user key.
kdf_params: Kdf::PBKDF2 {
iterations: 600_000.try_into().unwrap(),
},
email: "[email protected]".into(),
private_key: make_key_pair(user_key.to_string())
.unwrap()
.user_key_encrypted_private_key,
signing_key: None,
security_state: None,
method: InitUserCryptoMethod::PinEnvelope {
pin: test_pin.to_string(),
pin_protected_user_key_envelope: enroll_response
.pin_protected_user_key_envelope,
},
},
)
.await
.unwrap();
}

#[test]
fn test_enroll_admin_password_reset() {
let client = Client::new(None);
Expand Down
33 changes: 30 additions & 3 deletions crates/bitwarden-core/src/key_management/crypto_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ use crate::key_management::crypto::{
use crate::{
client::encryption_settings::EncryptionSettingsError,
error::StatefulCryptoError,
key_management::crypto::{
get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError,
UserCryptoV2KeysResponse,
key_management::{
crypto::{
enroll_pin, get_v2_rotated_account_keys, make_v2_keys_for_v1_user, CryptoClientError,
EnrollPinResponse, UserCryptoV2KeysResponse,
},
PasswordProtectedKeyEnvelope, SymmetricKeyId,
},
Client,
};
Expand Down Expand Up @@ -80,6 +83,30 @@ impl CryptoClient {
) -> Result<UserCryptoV2KeysResponse, StatefulCryptoError> {
get_v2_rotated_account_keys(&self.client)
}

/// Protects the current user key with 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 fn enroll_pin(&self, pin: String) -> Result<EnrollPinResponse, CryptoClientError> {
enroll_pin(&self.client, pin)
}

/// Decrypts a `PasswordProtectedKeyEnvelope`, returning the user key, if successful.
/// This is a stop-gap solution, until initialization of the SDK is used.
pub fn unseal_password_protected_key_envelope(
&self,
pin: String,
envelope: PasswordProtectedKeyEnvelope,
) -> Result<Vec<u8>, CryptoClientError> {
let mut ctx = self.client.internal.get_key_store().context_mut();
let key_slot = SymmetricKeyId::Local("unseal_password_protected_key_envelope");
envelope
.unseal(key_slot, pin.as_str(), &mut ctx)
.map_err(CryptoError::PasswordProtectedKeyEnvelopeError)?;
#[allow(deprecated)]
let key = ctx.dangerous_get_symmetric_key(key_slot)?;
Ok(key.to_encoded().to_vec())
}
}

impl CryptoClient {
Expand Down
4 changes: 4 additions & 0 deletions crates/bitwarden-core/src/key_management/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ mod crypto_client;
#[cfg(feature = "internal")]
pub use crypto_client::CryptoClient;

#[cfg(feature = "internal")]
mod non_generic_wrappers;
#[cfg(feature = "internal")]
pub(crate) use non_generic_wrappers::*;
#[cfg(feature = "internal")]
mod security_state;
#[cfg(feature = "internal")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Structs with generic parameters cannot be moved across FFI bounds (uniffi/wasm).
//! This module contains wrapper structs that hide the generic parameter with instantiated versions.

use std::ops::Deref;

use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;

use crate::key_management::KeyIds;

/// A non-generic wrapper around `bitwarden-crypto`'s `PasswordProtectedKeyEnvelope`.
#[derive(Serialize, Deserialize)]
#[serde(transparent)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct PasswordProtectedKeyEnvelope(
#[cfg_attr(
feature = "wasm",
tsify(type = r#"Tagged<string, "PasswordProtectedKeyEnvelope">"#)
)]
pub(crate) bitwarden_crypto::safe::PasswordProtectedKeyEnvelope<KeyIds>,
);

impl Deref for PasswordProtectedKeyEnvelope {
type Target = bitwarden_crypto::safe::PasswordProtectedKeyEnvelope<KeyIds>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl std::fmt::Debug for PasswordProtectedKeyEnvelope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
12 changes: 10 additions & 2 deletions crates/bitwarden-core/src/uniffi_support.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! This module contains custom type converters for Uniffi.

use std::num::NonZeroU32;
use std::{num::NonZeroU32, str::FromStr};

use bitwarden_crypto::CryptoError;
use uuid::Uuid;

use crate::key_management::SignedSecurityState;
use crate::key_management::{PasswordProtectedKeyEnvelope, SignedSecurityState};

uniffi::use_remote_type!(bitwarden_crypto::NonZeroU32);

Expand All @@ -18,6 +18,14 @@ uniffi::custom_type!(Uuid, String, {
lower: |obj| obj.to_string(),
});

uniffi::custom_type!(PasswordProtectedKeyEnvelope, String, {
remote,
try_lift: |val| bitwarden_crypto::safe::PasswordProtectedKeyEnvelope::from_str(val.as_str())
.map_err(|e| e.into())
.map(PasswordProtectedKeyEnvelope),
lower: |obj| obj.0.into(),
});

// Uniffi doesn't emit unused types, this is a dummy record to ensure that the custom type
// converters are emitted
#[allow(dead_code)]
Expand Down
15 changes: 3 additions & 12 deletions crates/bitwarden-crypto/examples/protect_key_with_password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,7 @@ fn main() {
let pin = "1234";
let envelope =
PasswordProtectedKeyEnvelope::seal(vault_key, pin, &ctx).expect("Sealing should work");
disk.save(
"vault_key_envelope",
(&envelope).try_into().expect("Saving envelope should work"),
);
disk.save("vault_key_envelope", (&envelope).into());

// Wipe the context to simulate new session
ctx.clear_local();
Expand All @@ -54,20 +51,14 @@ fn main() {
let envelope = envelope
.reseal(pin, "0000")
.expect("The password should be valid");
disk.save(
"vault_key_envelope",
(&envelope).try_into().expect("Saving envelope should work"),
);
disk.save("vault_key_envelope", (&envelope).into());

// Alice wants to change the protected key. This requires creating a new envelope
ctx.generate_symmetric_key(ExampleSymmetricKey::VaultKey)
.expect("Generating vault key should work");
let envelope = PasswordProtectedKeyEnvelope::seal(ExampleSymmetricKey::VaultKey, "0000", &ctx)
.expect("Sealing should work");
disk.save(
"vault_key_envelope",
(&envelope).try_into().expect("Saving envelope should work"),
);
disk.save("vault_key_envelope", (&envelope).into());

// Alice tries the password but it is wrong
assert!(matches!(
Expand Down
5 changes: 4 additions & 1 deletion crates/bitwarden-crypto/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bitwarden_error::bitwarden_error;
use thiserror::Error;
use uuid::Uuid;

use crate::fingerprint::FingerprintError;
use crate::{fingerprint::FingerprintError, safe::PasswordProtectedKeyEnvelopeError};

#[allow(missing_docs)]
#[bitwarden_error(flat)]
Expand Down Expand Up @@ -69,6 +69,9 @@ pub enum CryptoError {

#[error("Encoding error, {0}")]
EncodingError(#[from] EncodingError),

#[error("Password protected key envelope error, {0}")]
PasswordProtectedKeyEnvelopeError(#[from] PasswordProtectedKeyEnvelopeError),
}

#[derive(Debug, Error)]
Expand Down
Loading
Loading