Skip to content

Commit afb991b

Browse files
authored
[PM-27115] Force icon uri checksum verification on user crypto v2 (#519)
## 🎟️ Tracking https://bitwarden.atlassian.net/browse/PM-27115 https://bitwarden.atlassian.net/browse/VULN-185 https://bitwarden.atlassian.net/browse/PM-4185 ## 📔 Objective Forces the icon URI check for crypto v2 users. This adds some plumbing through the key store, which now also includes other non-key cryptographic state. The account security version is expected to be used in many other places that have access to the key store and we do not want to pass through an additional struct to all places. Please see the tickets for context about what specifically this change achieves. Further, this fixes the icon uri check to be constant time. ## ⏰ Reminders before review - Contributor guidelines followed - All formatters and local linters executed and passed - Written new unit and / or integration tests where applicable - Protected functional changes with optionality (feature flags) - Used internationalization (i18n) for all UI strings - CI builds passed - Communicated to DevOps any deployment requirements - Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team ## 🦮 Reviewer guidelines <!-- Suggested interactions but feel free to use (or not) as you desire! --> - 👍 (`:+1:`) or similar for great changes - 📝 (`:memo:`) or ℹ️ (`:information_source:`) for notes or general info - ❓ (`:question:`) for questions - 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion - 🎨 (`:art:`) for suggestions / improvements - ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention - 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt - ⛏ (`:pick:`) for minor or nitpick changes
1 parent b1cf684 commit afb991b

File tree

11 files changed

+43
-7
lines changed

11 files changed

+43
-7
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ serde_json = ">=1.0.96, <2.0"
7373
serde_qs = ">=0.12.0, <0.16"
7474
serde_repr = ">=0.1.12, <0.2"
7575
serde-wasm-bindgen = ">=0.6.0, <0.7"
76+
subtle = ">=2.5.0, <3.0"
7677
syn = ">=2.0.87, <3"
7778
thiserror = ">=1.0.40, <3"
7879
tokio = { version = "1.36.0", features = ["macros"] }

crates/bitwarden-core/src/client/encryption_settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ impl EncryptionSettings {
170170
let security_state: SecurityState = security_state
171171
.verify_and_unwrap(&signing_key.to_verifying_key())
172172
.map_err(|_| EncryptionSettingsError::InvalidSecurityState)?;
173+
store.set_security_state_version(security_state.version());
173174
*sdk_security_state.write().expect("RwLock not poisoned") = Some(security_state);
174175

175176
#[allow(deprecated)]

crates/bitwarden-core/src/key_management/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ pub(crate) use master_password::{MasterPasswordAuthenticationData, MasterPasswor
2828
#[cfg(feature = "internal")]
2929
mod security_state;
3030
#[cfg(feature = "internal")]
31-
pub use security_state::{SecurityState, SignedSecurityState};
31+
pub use security_state::{
32+
MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SecurityState, SignedSecurityState,
33+
};
3234
#[cfg(feature = "internal")]
3335
mod user_decryption;
3436
#[cfg(feature = "internal")]

crates/bitwarden-core/src/key_management/security_state.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ use serde::{Deserialize, Serialize};
3131

3232
use crate::UserId;
3333

34+
/// Icon URI hashes are enforced starting with this security state version.
35+
pub const MINIMUM_ENFORCE_ICON_URI_HASH_VERSION: u64 = 2;
36+
3437
#[cfg(feature = "wasm")]
3538
#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
3639
const TS_CUSTOM_TYPES: &'static str = r#"

crates/bitwarden-crypto/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ serde_bytes = { workspace = true }
5555
serde_repr.workspace = true
5656
sha1 = ">=0.10.5, <0.11"
5757
sha2 = ">=0.10.6, <0.11"
58-
subtle = ">=2.5.0, <3.0"
58+
subtle = { workspace = true }
5959
thiserror = { workspace = true }
6060
tsify = { workspace = true, optional = true }
6161
typenum = ">=1.18.0, <1.19.0"

crates/bitwarden-crypto/src/store/context.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ pub struct KeyStoreContext<'a, Ids: KeyIds> {
8282
pub(super) local_asymmetric_keys: Box<dyn StoreBackend<Ids::Asymmetric>>,
8383
pub(super) local_signing_keys: Box<dyn StoreBackend<Ids::Signing>>,
8484

85+
pub(super) security_state_version: u64,
86+
8587
// Make sure the context is !Send & !Sync
8688
pub(super) _phantom: std::marker::PhantomData<(Cell<()>, RwLockReadGuard<'static, ()>)>,
8789
}
@@ -122,6 +124,13 @@ impl<Ids: KeyIds> KeyStoreContext<'_, Ids> {
122124
self.local_signing_keys.clear();
123125
}
124126

127+
/// Returns the version of the security state of the key context. This describes the user's
128+
/// encryption version and can be used to disable certain old / dangerous format features
129+
/// safely.
130+
pub fn get_security_state_version(&self) -> u64 {
131+
self.security_state_version
132+
}
133+
125134
/// Remove all symmetric keys from the context for which the predicate returns false
126135
/// This will also remove the keys from the global store if this context has write access
127136
pub fn retain_symmetric_keys(&mut self, f: fn(Ids::Symmetric) -> bool) {

crates/bitwarden-crypto/src/store/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ struct KeyStoreInner<Ids: KeyIds> {
113113
symmetric_keys: Box<dyn StoreBackend<Ids::Symmetric>>,
114114
asymmetric_keys: Box<dyn StoreBackend<Ids::Asymmetric>>,
115115
signing_keys: Box<dyn StoreBackend<Ids::Signing>>,
116+
security_state_version: u64,
116117
}
117118

118119
/// Create a new key store with the best available implementation for the current platform.
@@ -123,6 +124,7 @@ impl<Ids: KeyIds> Default for KeyStore<Ids> {
123124
symmetric_keys: create_store(),
124125
asymmetric_keys: create_store(),
125126
signing_keys: create_store(),
127+
security_state_version: 1,
126128
})),
127129
}
128130
}
@@ -138,6 +140,12 @@ impl<Ids: KeyIds> KeyStore<Ids> {
138140
keys.signing_keys.clear();
139141
}
140142

143+
/// Sets the security state version for this store.
144+
pub fn set_security_state_version(&self, version: u64) {
145+
let mut data = self.inner.write().expect("RwLock is poisoned");
146+
data.security_state_version = version;
147+
}
148+
141149
/// Initiate an encryption/decryption context. This context will have read only access to the
142150
/// global keys, and will have its own local key stores with read/write access. This
143151
/// context-local store will be cleared when the context is dropped.
@@ -170,11 +178,14 @@ impl<Ids: KeyIds> KeyStore<Ids> {
170178
/// - [KeyStoreContext::encapsulate_key_unsigned]
171179
/// - [KeyStoreContext::derive_shareable_key]
172180
pub fn context(&'_ self) -> KeyStoreContext<'_, Ids> {
181+
let data = self.inner.read().expect("RwLock is poisoned");
182+
let security_state_version = data.security_state_version;
173183
KeyStoreContext {
174-
global_keys: GlobalKeys::ReadOnly(self.inner.read().expect("RwLock is poisoned")),
184+
global_keys: GlobalKeys::ReadOnly(data),
175185
local_symmetric_keys: create_store(),
176186
local_asymmetric_keys: create_store(),
177187
local_signing_keys: create_store(),
188+
security_state_version,
178189
_phantom: std::marker::PhantomData,
179190
}
180191
}
@@ -200,11 +211,14 @@ impl<Ids: KeyIds> KeyStore<Ids> {
200211
/// you're not holding references to the context across await points, as that would cause the
201212
/// future to also not be [Send].
202213
pub fn context_mut(&'_ self) -> KeyStoreContext<'_, Ids> {
214+
let inner = self.inner.write().expect("RwLock is poisoned");
215+
let security_state_version = inner.security_state_version;
203216
KeyStoreContext {
204-
global_keys: GlobalKeys::ReadWrite(self.inner.write().expect("RwLock is poisoned")),
217+
global_keys: GlobalKeys::ReadWrite(inner),
205218
local_symmetric_keys: create_store(),
206219
local_asymmetric_keys: create_store(),
207220
local_signing_keys: create_store(),
221+
security_state_version,
208222
_phantom: std::marker::PhantomData,
209223
}
210224
}

crates/bitwarden-vault/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ serde_json = { workspace = true }
5050
serde_repr = { workspace = true }
5151
sha1 = ">=0.10.5, <0.11"
5252
sha2 = ">=0.10.6, <0.11"
53+
subtle = { workspace = true }
5354
thiserror = { workspace = true }
5455
tsify = { workspace = true, optional = true }
5556
uniffi = { workspace = true, optional = true }

crates/bitwarden-vault/src/cipher/cipher.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use bitwarden_api_api::{
88
use bitwarden_collections::collection::CollectionId;
99
use bitwarden_core::{
1010
MissingFieldError, OrganizationId, UserId,
11-
key_management::{KeyIds, SymmetricKeyId},
11+
key_management::{KeyIds, MINIMUM_ENFORCE_ICON_URI_HASH_VERSION, SymmetricKeyId},
1212
require,
1313
};
1414
use bitwarden_crypto::{
@@ -554,7 +554,10 @@ impl Decryptable<KeyIds, SymmetricKeyId, CipherView> for Cipher {
554554
};
555555

556556
// For compatibility we only remove URLs with invalid checksums if the cipher has a key
557-
if cipher.key.is_some() {
557+
// or the user is on Crypto V2
558+
if cipher.key.is_some()
559+
|| ctx.get_security_state_version() >= MINIMUM_ENFORCE_ICON_URI_HASH_VERSION
560+
{
558561
cipher.remove_invalid_checksums();
559562
}
560563

0 commit comments

Comments
 (0)