Skip to content
Open
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
17 changes: 14 additions & 3 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ use std::collections::HashMap;

use bitwarden_crypto::{
AsymmetricCryptoKey, CoseSerializable, CryptoError, EncString, Kdf, KeyDecryptable,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, SignatureAlgorithm,
SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey, UnsignedSharedKey,
UserKey, dangerous_get_v2_rotated_account_keys, safe::PasswordProtectedKeyEnvelopeError,
KeyEncryptable, MasterKey, Pkcs8PrivateKeyBytes, PrimitiveEncryptable, RotateableKeySet,
SignatureAlgorithm, SignedPublicKey, SigningKey, SpkiPublicKeyBytes, SymmetricCryptoKey,
UnsignedSharedKey, UserKey, dangerous_get_v2_rotated_account_keys,
derive_symmetric_key_from_prf, safe::PasswordProtectedKeyEnvelopeError,
};
use bitwarden_encoding::B64;
use bitwarden_error::bitwarden_error;
Expand Down Expand Up @@ -515,6 +516,16 @@ fn derive_pin_protected_user_key(
Ok(derived_key.encrypt_user_key(user_key)?)
}

pub(super) fn make_prf_user_key_set(
client: &Client,
prf: B64,
) -> Result<RotateableKeySet, CryptoClientError> {
let prf_key = derive_symmetric_key_from_prf(prf.as_bytes())?;
let ctx = client.internal.get_key_store().context();
let key_set = RotateableKeySet::new(&ctx, &prf_key, SymmetricKeyId::User)?;
Ok(key_set)
}

#[allow(missing_docs)]
#[bitwarden_error(flat)]
#[derive(Debug, thiserror::Error)]
Expand Down
10 changes: 8 additions & 2 deletions crates/bitwarden-core/src/key_management/crypto_client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_crypto::{CryptoError, Decryptable, Kdf};
use bitwarden_crypto::{CryptoError, Decryptable, Kdf, RotateableKeySet};
#[cfg(feature = "internal")]
use bitwarden_crypto::{EncString, UnsignedSharedKey};
use bitwarden_encoding::B64;
Expand All @@ -18,7 +18,7 @@ use crate::key_management::{
crypto::{
DerivePinKeyResponse, InitOrgCryptoRequest, InitUserCryptoRequest, UpdatePasswordResponse,
derive_pin_key, derive_pin_user_key, enroll_admin_password_reset, get_user_encryption_key,
initialize_org_crypto, initialize_user_crypto,
initialize_org_crypto, initialize_user_crypto, make_prf_user_key_set,
},
};
use crate::{
Expand Down Expand Up @@ -171,6 +171,12 @@ impl CryptoClient {
derive_pin_user_key(&self.client, encrypted_pin)
}

/// Creates a new rotateable key set for the current user key protected
/// by a key derived from the given PRF.
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet, CryptoClientError> {
make_prf_user_key_set(&self.client, prf)
}

/// Prepares the account for being enrolled in the admin password reset feature. This encrypts
/// the users [UserKey][bitwarden_crypto::UserKey] with the organization's public key.
pub fn enroll_admin_password_reset(
Expand Down
4 changes: 4 additions & 0 deletions crates/bitwarden-crypto/src/keys/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ pub use kdf::{
default_pbkdf2_iterations,
};
pub(crate) use key_id::{KEY_ID_SIZE, KeyId};
mod prf;
mod rotateable_key_set;
pub use rotateable_key_set::RotateableKeySet;
pub(crate) mod utils;
pub use prf::derive_symmetric_key_from_prf;
56 changes: 56 additions & 0 deletions crates/bitwarden-crypto/src/keys/prf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use crate::{CryptoError, SymmetricCryptoKey, utils::stretch_key};

/// Takes the output of a PRF and derives a symmetric key.
///
/// The PRF output must be at least 32 bytes long.
pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result<SymmetricCryptoKey, CryptoError> {
let (secret, _) = prf.split_at_checked(32).ok_or(CryptoError::InvalidKeyLen)?;
Comment on lines +5 to +7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably mention in the docs that only the first 32 bytes of the PRF output are processed:

/// The PRF output must be at least 32 bytes long. If longer, only the first 32 bytes are used and the remainder is discarded.
pub fn derive_symmetric_key_from_prf(prf: &[u8]) -> Result<SymmetricCryptoKey, CryptoError> {

let secret: [u8; 32] = secret.try_into().expect("length to be 32 bytes");
// Don't allow uninitialized PRFs
if secret.iter().all(|b| *b == b'\0') {
return Err(CryptoError::ZeroNumber);
}
Ok(SymmetricCryptoKey::Aes256CbcHmacKey(stretch_key(
&Box::pin(secret.into()),
)?))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_prf_succeeds() {
let prf = pseudorandom_bytes(32);
let key = derive_symmetric_key_from_prf(&prf).unwrap();
assert!(matches!(key, SymmetricCryptoKey::Aes256CbcHmacKey(_)));
}

#[test]
fn test_zero_key_fails() {
let prf: Vec<u8> = (0..32).map(|_| 0).collect();
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
assert!(matches!(err, CryptoError::ZeroNumber));
}

#[test]
fn test_short_prf_fails() {
let prf = pseudorandom_bytes(9);
let err = derive_symmetric_key_from_prf(&prf).unwrap_err();
assert!(matches!(err, CryptoError::InvalidKeyLen));
}

#[test]
fn test_long_prf_truncated_to_proper_length() {
let long_prf = pseudorandom_bytes(33);
let prf = pseudorandom_bytes(32);
let key1 = derive_symmetric_key_from_prf(&long_prf).unwrap();
let key2 = derive_symmetric_key_from_prf(&prf).unwrap();
assert_eq!(key1, key2);
}

/// This returns the same bytes deterministically for a given length.
fn pseudorandom_bytes(len: usize) -> Vec<u8> {
(0..len).map(|x| (x % 255) as u8).collect()
}
}
202 changes: 202 additions & 0 deletions crates/bitwarden-crypto/src/keys/rotateable_key_set.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
use serde::{Deserialize, Serialize};

use crate::{
AsymmetricCryptoKey, AsymmetricPublicCryptoKey, CryptoError, EncString, KeyDecryptable,
KeyEncryptable, KeyIds, KeyStoreContext, Pkcs8PrivateKeyBytes, SpkiPublicKeyBytes,
SymmetricCryptoKey, UnsignedSharedKey,
};

/// A set of keys where a given `DownstreamKey` is protected by an encrypted public/private
/// key-pair. The `DownstreamKey` is used to encrypt/decrypt data, while the public/private key-pair
/// is used to rotate the `DownstreamKey`.
///
/// The `PrivateKey` is protected by an `UpstreamKey`, such as a `DeviceKey`, or `PrfKey`,
/// and the `PublicKey` is protected by the `DownstreamKey`. This setup allows:
///
/// - Access to `DownstreamKey` by knowing the `UpstreamKey`
/// - Rotation to a `NewDownstreamKey` by knowing the current `DownstreamKey`, without needing
/// access to the `UpstreamKey`
#[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),
tsify(into_wasm_abi, from_wasm_abi)
)]
pub struct RotateableKeySet {
/// `DownstreamKey` protected by encapsulation key
encapsulated_downstream_key: UnsignedSharedKey,
/// Encapsulation key protected by `DownstreamKey`
encrypted_encapsulation_key: EncString,
/// Decapsulation key protected by `UpstreamKey`
encrypted_decapsulation_key: EncString,
}

impl RotateableKeySet {
/// Create a set of keys to allow access to the downstream key via the provided
/// upstream key while allowing the downstream key to be rotated.
pub fn new<Ids: KeyIds>(
ctx: &KeyStoreContext<Ids>,
upstream_key: &SymmetricCryptoKey,
downstream_key_id: Ids::Symmetric,
) -> Result<Self, CryptoError> {
let key_pair = AsymmetricCryptoKey::make(crate::PublicKeyEncryptionAlgorithm::RsaOaepSha1);

// This uses this deprecated method and other methods directly on the other keys
// rather than the key store context because we don't want the keys to
// wind up being stored in the borrowed context.
#[allow(deprecated)]
let downstream_key = ctx.dangerous_get_symmetric_key(downstream_key_id)?;
// encapsulate downstream key
let encapsulated_downstream_key =
UnsignedSharedKey::encapsulate_key_unsigned(downstream_key, &key_pair.to_public_key())?;

// wrap decapsulation key with upstream key
let encrypted_decapsulation_key = key_pair.to_der()?.encrypt_with_key(upstream_key)?;

// wrap encapsulation key with downstream key
// Note: Usually, a public key is - by definition - public, so this should not be necessary.
// The specific use-case for this function is to enable rotateable key sets, where
// the "public key" is not public, with the intent of preventing the server from being able
// to overwrite the downstream key unlocked by the rotateable keyset.
let encrypted_encapsulation_key = key_pair
.to_public_key()
.to_der()?
.encrypt_with_key(downstream_key)?;

Ok(RotateableKeySet {
encapsulated_downstream_key,
encrypted_encapsulation_key,
encrypted_decapsulation_key,
})
}

// TODO: Eventually, the webauthn-login-strategy service should be migrated
// to use this method, and we can remove the #[allow(dead_code)] attribute.
#[allow(dead_code)]
fn unlock<Ids: KeyIds>(
&self,
ctx: &mut KeyStoreContext<Ids>,
upstream_key: &SymmetricCryptoKey,
downstream_key_id: Ids::Symmetric,
) -> Result<(), CryptoError> {
let priv_key_bytes: Vec<u8> = self
.encrypted_decapsulation_key
.decrypt_with_key(upstream_key)?;
let decapsulation_key =
AsymmetricCryptoKey::from_der(&Pkcs8PrivateKeyBytes::from(priv_key_bytes))?;
let downstream_key = self
.encapsulated_downstream_key
.decapsulate_key_unsigned(&decapsulation_key)?;
#[allow(deprecated)]
ctx.set_symmetric_key(downstream_key_id, downstream_key)?;
Ok(())
}
}

#[allow(dead_code)]
fn rotate_key_set<Ids: KeyIds>(
ctx: &KeyStoreContext<Ids>,
key_set: RotateableKeySet,
old_downstream_key_id: Ids::Symmetric,
new_downstream_key_id: Ids::Symmetric,
) -> Result<RotateableKeySet, CryptoError> {
let pub_key_bytes = ctx.decrypt_data_with_symmetric_key(
old_downstream_key_id,
&key_set.encrypted_encapsulation_key,
)?;
let pub_key = SpkiPublicKeyBytes::from(pub_key_bytes);
let encapsulation_key = AsymmetricPublicCryptoKey::from_der(&pub_key)?;
// TODO: There is no method to store only the public key in the store, so we
// have pull out the downstream key to encapsulate it manually.
#[allow(deprecated)]
let new_downstream_key = ctx.dangerous_get_symmetric_key(new_downstream_key_id)?;
let new_encapsulated_key =
UnsignedSharedKey::encapsulate_key_unsigned(new_downstream_key, &encapsulation_key)?;
let new_encrypted_encapsulation_key = pub_key.encrypt_with_key(new_downstream_key)?;
Ok(RotateableKeySet {
encapsulated_downstream_key: new_encapsulated_key,
encrypted_encapsulation_key: new_encrypted_encapsulation_key,
encrypted_decapsulation_key: key_set.encrypted_decapsulation_key,
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::{
KeyStore,
traits::tests::{TestIds, TestSymmKey},
};

#[test]
fn test_rotateable_key_set_can_unlock() {
// generate initial keys
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
// set up store
let store: KeyStore<TestIds> = KeyStore::default();
let mut ctx = store.context_mut();
let original_downstream_key_id = ctx.generate_symmetric_key();

// create key set
let key_set =
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();

// unlock key set
let unwrapped_downstream_key_id = TestSymmKey::A(1);
key_set
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
.unwrap();

#[allow(deprecated)]
let original_downstream_key = ctx
.dangerous_get_symmetric_key(original_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let unwrapped_downstream_key = ctx
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
.unwrap();
assert_eq!(original_downstream_key, unwrapped_downstream_key);
}

#[test]
fn test_rotateable_key_set_rotation() {
// generate initial keys
let upstream_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
// set up store
let store: KeyStore<TestIds> = KeyStore::default();
let mut ctx = store.context_mut();
let original_downstream_key_id = ctx.generate_symmetric_key();

// create key set
let key_set =
RotateableKeySet::new(&ctx, &upstream_key, original_downstream_key_id).unwrap();

// rotate
let new_downstream_key_id = ctx.generate_symmetric_key();
let new_key_set = rotate_key_set(
&ctx,
key_set,
original_downstream_key_id,
new_downstream_key_id,
)
.unwrap();

// After rotation, the new key set should be unlocked by the same
// upstream key and return the new downstream key.
let unwrapped_downstream_key_id = TestSymmKey::A(2_2);
new_key_set
.unlock(&mut ctx, &upstream_key, unwrapped_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let new_downstream_key = ctx
.dangerous_get_symmetric_key(new_downstream_key_id)
.unwrap();
#[allow(deprecated)]
let unwrapped_downstream_key = ctx
.dangerous_get_symmetric_key(unwrapped_downstream_key_id)
.unwrap();
assert_eq!(new_downstream_key, unwrapped_downstream_key);
}
}
8 changes: 7 additions & 1 deletion crates/bitwarden-uniffi/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bitwarden_core::key_management::crypto::{
DeriveKeyConnectorRequest, DerivePinKeyResponse, EnrollPinResponse, InitOrgCryptoRequest,
InitUserCryptoRequest, UpdateKdfResponse, UpdatePasswordResponse,
};
use bitwarden_crypto::{EncString, Kdf, UnsignedSharedKey};
use bitwarden_crypto::{EncString, Kdf, RotateableKeySet, UnsignedSharedKey};
use bitwarden_encoding::B64;

use crate::error::Result;
Expand Down Expand Up @@ -88,6 +88,12 @@ impl CryptoClient {
Ok(self.0.derive_key_connector(request)?)
}

/// Creates the a new rotateable key set for the current user key protected
/// by a key derived from the given PRF.
pub fn make_prf_user_key_set(&self, prf: B64) -> Result<RotateableKeySet> {
Ok(self.0.make_prf_user_key_set(prf)?)
}

/// Create the data necessary to update the user's kdf settings. The user's encryption key is
/// re-encrypted for the password under the new kdf settings. This returns the new encrypted
/// user key and the new password hash but does not update sdk state.
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-vault/src/cipher/cipher_client/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pub enum GetCipherError {
#[error(transparent)]
Crypto(#[from] CryptoError),
#[error(transparent)]
RepositoryError(#[from] RepositoryError),
Repository(#[from] RepositoryError),
}

async fn get_cipher(
Expand Down
8 changes: 6 additions & 2 deletions crates/bitwarden-vault/src/cipher/cipher_client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
use std::sync::Arc;

use bitwarden_core::{Client, OrganizationId};
use bitwarden_crypto::{CompositeEncryptable, IdentifyKey, SymmetricCryptoKey};
use bitwarden_crypto::IdentifyKey;
#[cfg(feature = "wasm")]
use bitwarden_crypto::{CompositeEncryptable, SymmetricCryptoKey};
#[cfg(feature = "wasm")]
use bitwarden_encoding::B64;
use bitwarden_state::repository::{Repository, RepositoryError};
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

use super::EncryptionContext;
#[cfg(feature = "wasm")]
use crate::Fido2CredentialFullView;
use crate::{
Cipher, CipherError, CipherListView, CipherView, DecryptError, EncryptError,
Fido2CredentialFullView, cipher::cipher::DecryptCipherListResult,
cipher::cipher::DecryptCipherListResult,
};

mod create;
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-vault/src/collection_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use bitwarden_collections::{
tree::{NodeItem, Tree},
};
use bitwarden_core::Client;
#[cfg(feature = "wasm")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "wasm")]
use tsify::Tsify;
Expand Down