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
16 changes: 6 additions & 10 deletions crates/bitwarden-core/src/client/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Flags {
/// Enable cipher key encryption.
#[serde(alias = "enableCipherKeyEncryption", alias = "cipher-key-encryption")]
pub enable_cipher_key_encryption: bool,

/// Enable strict cipher field decryption (propagates errors instead of nulling fields).
#[serde(alias = "pm-34500-strict-cipher-decryption")]
pub strict_cipher_decryption: bool,
Expand Down Expand Up @@ -41,30 +37,30 @@ mod tests {
fn test_load_empty_map() {
let map = std::collections::HashMap::new();
let flags = Flags::load_from_map(map);
assert!(!flags.enable_cipher_key_encryption);
assert!(!flags.strict_cipher_decryption);
}

#[test]
fn test_load_valid_map() {
let mut map = std::collections::HashMap::new();
map.insert("enableCipherKeyEncryption".into(), true);
map.insert("strict-cipher-decryption".into(), true);
let flags = Flags::load_from_map(map);
assert!(flags.enable_cipher_key_encryption);
assert!(flags.strict_cipher_decryption);
}

#[test]
fn test_load_valid_map_alias() {
let mut map = std::collections::HashMap::new();
map.insert("cipher-key-encryption".into(), true);
map.insert("pm-34500-strict-cipher-decryption".into(), true);
let flags = Flags::load_from_map(map);
assert!(flags.enable_cipher_key_encryption);
assert!(flags.strict_cipher_decryption);
}

#[test]
fn test_load_invalid_map() {
let mut map = std::collections::HashMap::new();
map.insert("thisIsNotAFlag".into(), true);
let flags = Flags::load_from_map(map);
assert!(!flags.enable_cipher_key_encryption);
assert!(!flags.strict_cipher_decryption);
}
}
16 changes: 6 additions & 10 deletions crates/bitwarden-core/src/client/flags_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,15 @@ mod tests {

// With no flags loaded yet, get should return defaults.
let initial = client.flags().get().await;
assert!(!initial.enable_cipher_key_encryption);
assert!(!initial.strict_cipher_decryption);

// Loading flags should persist them via the FLAGS setting.
let mut map = HashMap::new();
map.insert("enableCipherKeyEncryption".to_string(), true);
map.insert("pm-34500-strict-cipher-decryption".to_string(), true);
client.flags().load(map).await;

// get should now return the loaded values.
let loaded = client.flags().get().await;
assert!(loaded.enable_cipher_key_encryption);
assert!(loaded.strict_cipher_decryption);

// The values should be readable directly from the setting too.
Expand All @@ -182,7 +179,6 @@ mod tests {
.await
.unwrap()
.expect("flags should be persisted after load");
assert!(persisted.enable_cipher_key_encryption);
assert!(persisted.strict_cipher_decryption);
}

Expand All @@ -192,7 +188,7 @@ mod tests {
Mock::given(method("GET"))
.and(path("/config"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"featureStates": { "enableCipherKeyEncryption": true }
"featureStates": { "pm-34500-strict-cipher-decryption": true }
})))
.expect(1)
.mount(&server)
Expand All @@ -202,7 +198,7 @@ mod tests {
let before = Utc::now();
client.flags().fetch(true).await.unwrap();

assert!(client.flags().get().await.enable_cipher_key_encryption);
assert!(client.flags().get().await.strict_cipher_decryption);
let fetched_at = read_fetched_at(&client)
.await
.expect("fetched_at must be set after a successful fetch");
Expand Down Expand Up @@ -247,7 +243,7 @@ mod tests {
Mock::given(method("GET"))
.and(path("/config"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"featureStates": { "enableCipherKeyEncryption": true }
"featureStates": { "pm-34500-strict-cipher-decryption": true }
})))
.expect(1)
.mount(&server)
Expand All @@ -259,7 +255,7 @@ mod tests {

client.flags().fetch(false).await.unwrap();

assert!(client.flags().get().await.enable_cipher_key_encryption);
assert!(client.flags().get().await.strict_cipher_decryption);
let fetched_at = read_fetched_at(&client).await.unwrap();
assert!(fetched_at > stale);
}
Expand All @@ -277,14 +273,14 @@ mod tests {
client
.flags()
.load(HashMap::from([(
"enableCipherKeyEncryption".to_string(),
"pm-34500-strict-cipher-decryption".to_string(),
true,
)]))
.await;

assert!(client.flags().fetch(true).await.is_err());
assert!(
client.flags().get().await.enable_cipher_key_encryption,
client.flags().get().await.strict_cipher_decryption,
"previously persisted flags must survive a failed fetch"
);
}
Expand Down
8 changes: 0 additions & 8 deletions crates/bitwarden-core/src/client/test_accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,6 @@ impl Client {
}

async fn init_test_account_on(client: Client, account: TestAccount) -> Self {
client
.flags()
.load(HashMap::from([(
"enableCipherKeyEncryption".to_owned(),
true,
)]))
.await;

client
.crypto()
.initialize_user_crypto(account.user)
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-importers/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ mod tests {
}));
let cipher = encrypt_import(&mut ctx, importing("GitHub", login), None).unwrap();

// Imported ciphers carry their own cipher key
assert!(cipher.key.is_some());
assert_ne!(cipher.name.unwrap().to_string(), "GitHub");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,35 @@ mod tests {
);
}

/// Builds a legacy cipher whose fields are encrypted directly under `key`, with no per-cipher
/// key. Encrypting a `CipherView` always produces one now, so this shape β€” which still exists
/// server-side for vaults predating per-cipher keys β€” has to be constructed by hand.
fn make_keyless_legacy_cipher(
view: &bitwarden_vault::CipherView,
key: bitwarden_core::key_management::SymmetricKeySlotId,
ctx: &mut bitwarden_crypto::KeyStoreContext<KeySlotIds>,
) -> Cipher {
use bitwarden_crypto::PrimitiveEncryptable;
use bitwarden_vault::Login;

let login = view.login.as_ref().unwrap();

Cipher {
name: Some(view.name.encrypt(ctx, key).unwrap()),
notes: view.notes.encrypt(ctx, key).unwrap(),
login: Some(Login {
username: login.username.encrypt(ctx, key).unwrap(),
password: login.password.encrypt(ctx, key).unwrap(),
password_revision_date: None,
uris: None,
totp: None,
autofill_on_page_load: None,
fido2_credentials: None,
}),
..make_test_cipher(None)
}
}

#[test]
fn test_ciphers() {
let store: KeyStore<KeySlotIds> = KeyStore::default();
Expand All @@ -381,9 +410,8 @@ mod tests {
ctx.make_symmetric_key(bitwarden_crypto::SymmetricKeyAlgorithm::Aes256CbcHmac);

let cipher = make_cipher_view();
let encrypted_cipher = EncryptMode::Legacy(cipher.clone())
.encrypt_composite(&mut ctx, user_key_old)
.unwrap();
let encrypted_cipher = make_keyless_legacy_cipher(&cipher, user_key_old, &mut ctx);
assert!(encrypted_cipher.key.is_none());

// Rotate it
let ciphers = vec![encrypted_cipher];
Expand All @@ -408,7 +436,7 @@ mod tests {

// A legacy cipher that already carries a per-item cipher key
let mut cipher = make_cipher_view();
cipher.generate_cipher_key(&mut ctx).unwrap();
let _ = cipher.load_cipher_key_slot(&mut ctx).unwrap();
let encrypted = EncryptMode::Legacy(cipher.clone())
.encrypt_composite(&mut ctx, user_key_old)
.unwrap();
Expand Down
22 changes: 5 additions & 17 deletions crates/bitwarden-vault/src/cipher/blob/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,14 @@ pub(crate) fn encrypt_blob_cipher(

/// Variant of [`encrypt_blob_cipher`] that accepts an explicit outer wrapping
/// key. Used by key rotation, where the new user/org key is installed under a
/// `Local` slot id and `view.key` has been rewrapped under that slot β€” calling
/// `key_identifier()` would resolve to the original `User`/`Organization` slot
/// and fail to unwrap the CEK.
/// `Local` slot id β€” `key_identifier()` would resolve to the original
/// `User`/`Organization` slot and wrap the cipher key under the old key.
pub(crate) fn encrypt_blob_cipher_with_wrapping_key(
view: &mut CipherView,
ctx: &mut KeyStoreContext<KeySlotIds>,
wrapping_key: SymmetricKeySlotId,
) -> Result<Cipher, BlobEncryptionError> {
if view.key.is_none() {
view.generate_cipher_key(ctx)?;
}

let cipher_key = view.load_cipher_key_slot(ctx, wrapping_key)?;
let cipher_key = view.load_cipher_key_slot(ctx)?;

let sealed_string = seal_cipher(view, ctx, cipher_key)?;

Expand All @@ -113,11 +108,7 @@ pub(crate) fn encrypt_blob_cipher_with_wrapping_key(
organization_id: view.organization_id,
folder_id: view.folder_id,
collection_ids: view.collection_ids.clone(),
key: view
.key
.as_ref()
.map(|_| ctx.wrap_symmetric_key(wrapping_key, cipher_key))
.transpose()?,
key: Some(ctx.wrap_symmetric_key(wrapping_key, cipher_key)?),
r#type: view.r#type,
favorite: view.favorite,
reprompt: view.reprompt,
Expand Down Expand Up @@ -319,11 +310,8 @@ mod tests {
view.secure_note = Some(SecureNoteView {
r#type: SecureNoteType::Generic,
});
view.generate_cipher_key(&mut ctx).unwrap();
let cipher_key = view.load_cipher_key_slot(&mut ctx).unwrap();

let cipher_key = view
.load_cipher_key_slot(&mut ctx, view.key_identifier())
.unwrap();
let sealed_string = seal_cipher(&view, &mut ctx, cipher_key).unwrap();

let mut cipher = make_test_cipher_with_data(&mut ctx, Some(sealed_string));
Expand Down
Loading
Loading