Skip to main content

bitwarden_core/key_management/
security_state.rs

1//! Security state is a signed object that attests to a user's (or later an organization's) security
2//! state. The security goal is to prevent downgrades of specific features within the user's account
3//! by the server / a networked attacker with TLS introspection access.
4//!
5//! A security state contains a security version. Based on this version, features can be disabled.
6//! Since the server cannot sign a security state, it can no longer downgrade the feature, because
7//! it cannot produce an arbitrary valid signed security state.
8//!
9//! Note: A long-term compromised server can record the security state of a user, and then replay
10//! this specific state, or the entire account to downgrade users to previous states. This can be
11//! prevented per logged in session by the client, and for bootstrapping a client by
12//! using an extended login-with-device protocol.
13//!
14//! To utilize the security state to disable a feature the following steps are taken:
15//! 1. Assume: Feature with format version A is insecure, and cannot be changed by simple mutation
16//! 2. A new, safe format version B is introduced, and an upgrade path created
17//! 3. The upgrade path is made mandatory
18//! 4. After upgrades are run, the sdk validates that all items are in format version B, and the
19//!    security state can be updated to contain the security version N+1
20//! 5. The client, given a security state with security version N+1 will reject all items that are
21//!    in format version A.
22
23use std::{fmt::Debug, str::FromStr};
24
25use bitwarden_crypto::{
26    CoseSerializable, CoseSign1Bytes, CryptoError, EncodingError, KeySlotIds, KeyStoreContext,
27    SignedObject, SigningNamespace, VerifyingKey,
28};
29use bitwarden_encoding::{B64, FromStrVisitor};
30use serde::{Deserialize, Serialize};
31
32/// Icon URI hashes are enforced starting with this security state version.
33pub const MINIMUM_ENFORCE_ICON_URI_HASH_VERSION: u64 = 2;
34
35/// Cipher blob encryption is enabled starting with this security state version.
36pub const BLOB_SECURITY_VERSION: u64 = 2;
37
38#[cfg(feature = "wasm")]
39#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
40const TS_CUSTOM_TYPES: &'static str = r#"
41export type SignedSecurityState = string;
42"#;
43
44/// The security state is a signed object attesting to the security state of a user.
45///
46/// It contains a version, which can only ever increment. Based on the version, old formats and
47/// features are blocked. This prevents a server from downgrading a user's account features, because
48/// only the user can create this signed object.
49#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SecurityState {
53    /// The version of the security state gates feature availability. It can only ever be
54    /// incremented. Components can use it to gate format support of specific formats (like
55    /// item url hashes).
56    version: u64,
57}
58
59impl Default for SecurityState {
60    fn default() -> Self {
61        Self::new()
62    }
63}
64
65impl SecurityState {
66    /// Initialize a new `SecurityState` for the given user ID, to the lowest version possible.
67    /// The user needs to be a v2 encryption user.
68    pub fn new() -> Self {
69        SecurityState { version: 2 }
70    }
71
72    /// Returns the version of the security state
73    pub fn version(&self) -> u64 {
74        self.version
75    }
76
77    /// Signs the `SecurityState` with the provided signing key ID from the context.
78    pub fn sign<Ids: KeySlotIds>(
79        &self,
80        signing_key_id: Ids::Signing,
81        ctx: &mut KeyStoreContext<Ids>,
82    ) -> Result<SignedSecurityState, CryptoError> {
83        Ok(SignedSecurityState(ctx.sign(
84            signing_key_id,
85            &self,
86            &SigningNamespace::SecurityState,
87        )?))
88    }
89}
90
91/// A signed and serialized `SecurityState` object.
92#[derive(Clone, PartialEq)]
93pub struct SignedSecurityState(pub(crate) SignedObject);
94
95impl Debug for SignedSecurityState {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let mut debug_struct = f.debug_struct("SignedSecurityState");
98
99        if let Ok(signed_by) = self.0.signed_by_id() {
100            debug_struct.field("signed_by", &signed_by);
101        }
102        if let Some(state) = self
103            .0
104            .dangerous_unverified_decode_do_not_use_except_for_debug_logs::<SecurityState>()
105        {
106            debug_struct.field("version", &state.version);
107        }
108
109        debug_struct.finish()
110    }
111}
112
113impl SignedSecurityState {
114    /// Verifies the signature of the `SignedSecurityState` using the provided `VerifyingKey`.
115    pub fn verify_and_unwrap(
116        self,
117        verifying_key: &VerifyingKey,
118    ) -> Result<SecurityState, CryptoError> {
119        self.0
120            .verify_and_unwrap(verifying_key, &SigningNamespace::SecurityState)
121    }
122}
123
124impl From<SignedSecurityState> for CoseSign1Bytes {
125    fn from(val: SignedSecurityState) -> Self {
126        val.0.to_cose()
127    }
128}
129
130impl TryFrom<&CoseSign1Bytes> for SignedSecurityState {
131    type Error = EncodingError;
132    fn try_from(bytes: &CoseSign1Bytes) -> Result<Self, EncodingError> {
133        Ok(SignedSecurityState(SignedObject::from_cose(bytes)?))
134    }
135}
136
137impl From<&SignedSecurityState> for String {
138    fn from(val: &SignedSecurityState) -> Self {
139        val.to_owned().into()
140    }
141}
142
143impl From<SignedSecurityState> for String {
144    fn from(val: SignedSecurityState) -> Self {
145        let bytes: CoseSign1Bytes = val.into();
146        B64::from(bytes.as_ref()).to_string()
147    }
148}
149
150impl FromStr for SignedSecurityState {
151    type Err = EncodingError;
152
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        let bytes = B64::try_from(s).map_err(|_| EncodingError::InvalidBase64Encoding)?;
155        Self::try_from(&CoseSign1Bytes::from(&bytes))
156    }
157}
158
159impl<'de> Deserialize<'de> for SignedSecurityState {
160    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
161    where
162        D: serde::Deserializer<'de>,
163    {
164        deserializer.deserialize_str(FromStrVisitor::new())
165    }
166}
167
168impl serde::Serialize for SignedSecurityState {
169    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170    where
171        S: serde::Serializer,
172    {
173        let b64_serialized_signed_public_key: String = self.clone().into();
174        serializer.serialize_str(&b64_serialized_signed_public_key)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use bitwarden_crypto::{CoseKeyBytes, KeyStore, SignatureAlgorithm, SigningKey};
181
182    use super::*;
183    use crate::key_management::KeySlotIds;
184
185    const TEST_SIGNED_SECURITY_STATE: &str = "hFgepAEnAxg8BFBHo5ojcqDqbynNymOZGgJzOgABOH8CoFgkomhlbnRpdHlJZFBHmj2OTpBFO7aDLgeNnbZPZ3ZlcnNpb24CWEA4mQbYRRoPpc77tVHH4LlwY52Vz6tutThv8b/BV3ntQmjuKUxbzIGRxSyOhzCn3ouFJGEVnfsl6SqSm6K9XcME";
186    const TEST_VERIFYING_KEY: &str =
187        "pgEBAlBHo5ojcqDqbynNymOZGgJzAycEgQIgBiFYIK9hIvbLIdnzKhykPt8jT/ktXAlzPUfx4Nyx4EYTpIp7";
188
189    #[test]
190    #[ignore = "Manual test for debug logs"]
191    fn test_security_state_debug_logs() {
192        let store: KeyStore<KeySlotIds> = KeyStore::default();
193        let mut ctx = store.context_mut();
194
195        let security_state = SecurityState::new();
196        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
197        let key = ctx.add_local_signing_key(signing_key.clone());
198        let signed_security_state = security_state.sign(key, &mut ctx).unwrap();
199
200        println!("{:?}", signed_security_state);
201    }
202
203    #[test]
204    #[ignore = "Make test vectors"]
205    fn test_make_test_vector() {
206        let store: KeyStore<KeySlotIds> = KeyStore::default();
207        let mut ctx = store.context_mut();
208
209        let security_state = SecurityState::new();
210        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
211        let key = ctx.add_local_signing_key(signing_key.clone());
212        let signed_security_state = security_state.sign(key, &mut ctx).unwrap();
213        let verifying_key = signing_key.to_verifying_key();
214
215        println!(
216            "const TEST_SIGNED_SECURITY_STATE: &str = \"{}\";",
217            String::from(&signed_security_state)
218        );
219        println!(
220            "const TEST_VERIFYING_KEY: &str = \"{}\";",
221            B64::from(verifying_key.to_cose())
222        );
223    }
224
225    #[test]
226    fn test_security_state_signing() {
227        let store: KeyStore<KeySlotIds> = KeyStore::default();
228        let mut ctx = store.context_mut();
229
230        let security_state = SecurityState::new();
231        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
232        let key = ctx.add_local_signing_key(signing_key.clone());
233        let signed_security_state = security_state.sign(key, &mut ctx).unwrap();
234
235        let verifying_key = signing_key.to_verifying_key();
236        let verified_security_state = signed_security_state
237            .verify_and_unwrap(&verifying_key)
238            .unwrap();
239
240        assert_eq!(verified_security_state.version(), 2);
241    }
242
243    #[test]
244    fn test_stable_testvector() {
245        let b64 = B64::try_from(TEST_VERIFYING_KEY).unwrap();
246        let verifying_key = VerifyingKey::from_cose(&CoseKeyBytes::from(&b64)).unwrap();
247        let signed_security_state =
248            SignedSecurityState::from_str(TEST_SIGNED_SECURITY_STATE).unwrap();
249        let verified_security_state = signed_security_state
250            .verify_and_unwrap(&verifying_key)
251            .unwrap();
252
253        assert_eq!(verified_security_state.version(), 2);
254    }
255}