diff --git a/crates/bitwarden-core/src/client/encryption_settings.rs b/crates/bitwarden-core/src/client/encryption_settings.rs index 64e692fcf..c351cc21c 100644 --- a/crates/bitwarden-core/src/client/encryption_settings.rs +++ b/crates/bitwarden-core/src/client/encryption_settings.rs @@ -48,6 +48,9 @@ pub enum EncryptionSettingsError { #[error(transparent)] UserIdAlreadySetError(#[from] UserIdAlreadySetError), + + #[error("Wrong Pin")] + WrongPin, } #[allow(clippy::large_enum_variant)] diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 847ef16d5..7a249f95c 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -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. @@ -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, diff --git a/crates/bitwarden-core/src/key_management/crypto.rs b/crates/bitwarden-core/src/key_management/crypto.rs index 800ce8085..da56f703a 100644 --- a/crates/bitwarden-core/src/key_management/crypto.rs +++ b/crates/bitwarden-core/src/key_management/crypto.rs @@ -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; @@ -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, }; @@ -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 { @@ -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`. @@ -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, @@ -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 { + 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)] @@ -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: "test@bitwarden.com".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: "test@bitwarden.com".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); diff --git a/crates/bitwarden-core/src/key_management/crypto_client.rs b/crates/bitwarden-core/src/key_management/crypto_client.rs index 961b3d19b..9f4366a1b 100644 --- a/crates/bitwarden-core/src/key_management/crypto_client.rs +++ b/crates/bitwarden-core/src/key_management/crypto_client.rs @@ -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, }; @@ -80,6 +83,30 @@ impl CryptoClient { ) -> Result { 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 { + 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, 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 { diff --git a/crates/bitwarden-core/src/key_management/mod.rs b/crates/bitwarden-core/src/key_management/mod.rs index 10be377a8..76d0195f6 100644 --- a/crates/bitwarden-core/src/key_management/mod.rs +++ b/crates/bitwarden-core/src/key_management/mod.rs @@ -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")] diff --git a/crates/bitwarden-core/src/key_management/non_generic_wrappers.rs b/crates/bitwarden-core/src/key_management/non_generic_wrappers.rs new file mode 100644 index 000000000..cb666a304 --- /dev/null +++ b/crates/bitwarden-core/src/key_management/non_generic_wrappers.rs @@ -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"#) + )] + pub(crate) bitwarden_crypto::safe::PasswordProtectedKeyEnvelope, +); + +impl Deref for PasswordProtectedKeyEnvelope { + type Target = bitwarden_crypto::safe::PasswordProtectedKeyEnvelope; + + 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) + } +} diff --git a/crates/bitwarden-core/src/uniffi_support.rs b/crates/bitwarden-core/src/uniffi_support.rs index 059c7510e..0b9094baf 100644 --- a/crates/bitwarden-core/src/uniffi_support.rs +++ b/crates/bitwarden-core/src/uniffi_support.rs @@ -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); @@ -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)] diff --git a/crates/bitwarden-crypto/examples/protect_key_with_password.rs b/crates/bitwarden-crypto/examples/protect_key_with_password.rs index 7cb0a5cee..a9cb35f61 100644 --- a/crates/bitwarden-crypto/examples/protect_key_with_password.rs +++ b/crates/bitwarden-crypto/examples/protect_key_with_password.rs @@ -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(); @@ -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!( diff --git a/crates/bitwarden-crypto/src/error.rs b/crates/bitwarden-crypto/src/error.rs index f572cec94..2d6c5cdc6 100644 --- a/crates/bitwarden-crypto/src/error.rs +++ b/crates/bitwarden-crypto/src/error.rs @@ -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)] @@ -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)] diff --git a/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs b/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs index 1aeea7f2b..1720ae4b7 100644 --- a/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs +++ b/crates/bitwarden-crypto/src/safe/password_protected_key_envelope.rs @@ -1,11 +1,13 @@ -use std::{marker::PhantomData, num::TryFromIntError}; +use std::{marker::PhantomData, num::TryFromIntError, str::FromStr}; use argon2::Params; +use base64::{engine::general_purpose::STANDARD, Engine}; use ciborium::{value::Integer, Value}; use coset::{ iana::CoapContentFormat, CborSerializable, ContentType, CoseError, Header, HeaderBuilder, }; use rand::RngCore; +use serde::{Deserialize, Serialize}; use thiserror::Error; use crate::{ @@ -13,8 +15,8 @@ use crate::{ extract_bytes, extract_integer, CoseExtractError, ALG_ARGON2ID13, ARGON2_ITERATIONS, ARGON2_MEMORY, ARGON2_PARALLELISM, ARGON2_SALT, CONTENT_TYPE_BITWARDEN_LEGACY_KEY, }, - xchacha20, BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, KeyIds, - KeyStoreContext, SymmetricCryptoKey, + xchacha20, BitwardenLegacyKeyBytes, ContentFormat, CoseKeyBytes, EncodedSymmetricKey, + FromStrVisitor, KeyIds, KeyStoreContext, SymmetricCryptoKey, }; /// A password-protected key envelope can seal a symmetric key, and protect it with a password. It @@ -209,11 +211,12 @@ impl PasswordProtectedKeyEnvelope { } } -impl TryInto> for &PasswordProtectedKeyEnvelope { - type Error = CoseError; - - fn try_into(self) -> Result, Self::Error> { - self.cose_encrypt.clone().to_vec() +impl From<&PasswordProtectedKeyEnvelope> for Vec { + fn from(val: &PasswordProtectedKeyEnvelope) -> Self { + val.cose_encrypt + .clone() + .to_vec() + .expect("Serialization to cose should not fail") } } @@ -229,6 +232,57 @@ impl TryFrom<&Vec> for PasswordProtectedKeyEnvelope { } } +impl std::fmt::Debug for PasswordProtectedKeyEnvelope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("PasswordProtectedKeyEnvelope") + .field("cose_encrypt", &self.cose_encrypt) + .finish() + } +} + +impl FromStr for PasswordProtectedKeyEnvelope { + type Err = PasswordProtectedKeyEnvelopeError; + + fn from_str(s: &str) -> Result { + let data = STANDARD.decode(s).map_err(|_| { + PasswordProtectedKeyEnvelopeError::ParsingError( + "Invalid PasswordProtectedKeyEnvelope Base64 encoding".to_string(), + ) + })?; + Self::try_from(&data).map_err(|_| { + PasswordProtectedKeyEnvelopeError::ParsingError( + "Failed to parse PasswordProtectedKeyEnvelope".to_string(), + ) + }) + } +} + +impl From> for String { + fn from(val: PasswordProtectedKeyEnvelope) -> Self { + let serialized: Vec = (&val).into(); + STANDARD.encode(serialized) + } +} + +impl<'de, Ids: KeyIds> Deserialize<'de> for PasswordProtectedKeyEnvelope { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(FromStrVisitor::new()) + } +} + +impl Serialize for PasswordProtectedKeyEnvelope { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let serialized: Vec = self.into(); + serializer.serialize_str(&STANDARD.encode(serialized)) + } +} + /// Raw argon2 settings differ from the KDF struct defined for existing master-password unlock. /// The memory is represented in kibibytes (KiB) instead of mebibytes (MiB), and the salt is a fixed /// size of 32 bytes, and randomly generated, instead of being derived from the email. @@ -471,7 +525,7 @@ mod tests { // Seal the key with a password let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap(); - let serialized: Vec = (&envelope).try_into().unwrap(); + let serialized: Vec = (&envelope).into(); // Unseal the key from the envelope let deserialized: PasswordProtectedKeyEnvelope = @@ -504,7 +558,7 @@ mod tests { // Seal the key with a password let envelope = PasswordProtectedKeyEnvelope::seal(test_key, password, &ctx).unwrap(); - let serialized: Vec = (&envelope).try_into().unwrap(); + let serialized: Vec = (&envelope).into(); // Unseal the key from the envelope let deserialized: PasswordProtectedKeyEnvelope = @@ -563,7 +617,7 @@ mod tests { // Attempt to unseal with the wrong password let deserialized: PasswordProtectedKeyEnvelope = - PasswordProtectedKeyEnvelope::try_from(&(&envelope).try_into().unwrap()).unwrap(); + PasswordProtectedKeyEnvelope::try_from(&(&envelope).into()).unwrap(); assert!(matches!( deserialized.unseal(TestSymmKey::A(1), wrong_password, &mut ctx), Err(PasswordProtectedKeyEnvelopeError::WrongPassword)