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::str::FromStr;
24
25use bitwarden_crypto::{
26    CoseSerializable, CoseSign1Bytes, CryptoError, EncodingError, KeyIds, KeyStoreContext,
27    SignedObject, SigningNamespace, VerifyingKey,
28};
29use bitwarden_encoding::{B64, FromStrVisitor};
30use serde::{Deserialize, Serialize};
31
32use crate::UserId;
33
34/// Icon URI hashes are enforced starting with this security state version.
35pub const MINIMUM_ENFORCE_ICON_URI_HASH_VERSION: u64 = 2;
36
37#[cfg(feature = "wasm")]
38#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)]
39const TS_CUSTOM_TYPES: &'static str = r#"
40export type SignedSecurityState = string;
41"#;
42
43/// The security state is a signed object attesting to the security state of a user.
44///
45/// It contains a version, which can only ever increment. Based on the version, old formats and
46/// features are blocked. This prevents a server from downgrading a user's account features, because
47/// only the user can create this signed object.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct SecurityState {
51    /// The entity ID is a permanent, unchangeable, unique identifier for the object this security
52    /// state applies to. For users, this is the user ID, which never changes.
53    entity_id: UserId,
54    /// The version of the security state gates feature availability. It can only ever be
55    /// incremented. Components can use it to gate format support of specific formats (like
56    /// item url hashes).
57    version: u64,
58}
59
60impl SecurityState {
61    /// Initialize a new `SecurityState` for the given user ID, to the lowest version possible.
62    /// The user needs to be a v2 encryption user.
63    pub fn initialize_for_user(user_id: UserId) -> Self {
64        SecurityState {
65            entity_id: user_id,
66            version: 2,
67        }
68    }
69
70    /// Returns the version of the security state
71    pub fn version(&self) -> u64 {
72        self.version
73    }
74
75    /// Signs the `SecurityState` with the provided signing key ID from the context.
76    pub fn sign<Ids: KeyIds>(
77        &self,
78        signing_key_id: Ids::Signing,
79        ctx: &mut KeyStoreContext<Ids>,
80    ) -> Result<SignedSecurityState, CryptoError> {
81        Ok(SignedSecurityState(ctx.sign(
82            signing_key_id,
83            &self,
84            &SigningNamespace::SecurityState,
85        )?))
86    }
87}
88
89/// A signed and serialized `SecurityState` object.
90#[derive(Clone, Debug)]
91pub struct SignedSecurityState(pub(crate) SignedObject);
92
93impl SignedSecurityState {
94    /// Verifies the signature of the `SignedSecurityState` using the provided `VerifyingKey`.
95    pub fn verify_and_unwrap(
96        self,
97        verifying_key: &VerifyingKey,
98    ) -> Result<SecurityState, CryptoError> {
99        self.0
100            .verify_and_unwrap(verifying_key, &SigningNamespace::SecurityState)
101    }
102}
103
104impl From<SignedSecurityState> for CoseSign1Bytes {
105    fn from(val: SignedSecurityState) -> Self {
106        val.0.to_cose()
107    }
108}
109
110impl TryFrom<&CoseSign1Bytes> for SignedSecurityState {
111    type Error = EncodingError;
112    fn try_from(bytes: &CoseSign1Bytes) -> Result<Self, EncodingError> {
113        Ok(SignedSecurityState(SignedObject::from_cose(bytes)?))
114    }
115}
116
117impl From<&SignedSecurityState> for String {
118    fn from(val: &SignedSecurityState) -> Self {
119        val.to_owned().into()
120    }
121}
122
123impl From<SignedSecurityState> for String {
124    fn from(val: SignedSecurityState) -> Self {
125        let bytes: CoseSign1Bytes = val.into();
126        B64::from(bytes.as_ref()).to_string()
127    }
128}
129
130impl FromStr for SignedSecurityState {
131    type Err = EncodingError;
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        let bytes = B64::try_from(s).map_err(|_| EncodingError::InvalidBase64Encoding)?;
135        Self::try_from(&CoseSign1Bytes::from(&bytes))
136    }
137}
138
139impl<'de> Deserialize<'de> for SignedSecurityState {
140    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141    where
142        D: serde::Deserializer<'de>,
143    {
144        deserializer.deserialize_str(FromStrVisitor::new())
145    }
146}
147
148impl serde::Serialize for SignedSecurityState {
149    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
150    where
151        S: serde::Serializer,
152    {
153        let b64_serialized_signed_public_key: String = self.clone().into();
154        serializer.serialize_str(&b64_serialized_signed_public_key)
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use bitwarden_crypto::{KeyStore, SignatureAlgorithm, SigningKey};
161
162    use super::*;
163    use crate::key_management::KeyIds;
164
165    #[test]
166    fn test_security_state_signing() {
167        let store: KeyStore<KeyIds> = KeyStore::default();
168        let mut ctx = store.context_mut();
169
170        let user_id = UserId::new_v4();
171        let security_state = SecurityState::initialize_for_user(user_id);
172        let signing_key = SigningKey::make(SignatureAlgorithm::Ed25519);
173        let key = ctx.add_local_signing_key(signing_key.clone()).unwrap();
174        let signed_security_state = security_state.sign(key, &mut ctx).unwrap();
175
176        let verifying_key = signing_key.to_verifying_key();
177        let verified_security_state = signed_security_state
178            .verify_and_unwrap(&verifying_key)
179            .unwrap();
180
181        assert_eq!(verified_security_state.entity_id, user_id);
182        assert_eq!(verified_security_state.version(), 2);
183    }
184}