Skip to main content

bitwarden_core/key_management/
v2_upgrade_token.rs

1//! V2 Upgrade Token is created during V1→V2 key rotation and holds both user keys wrapped by
2//! each other. This allows V1 devices to retrieve the V2 key (to complete the upgrade), and V2
3//! devices to retrieve the V1 key (e.g. to rotate local device unlock methods still encrypted
4//! with V1).
5//!
6//! On unwrapping, both directions are validated - an attacker can't modify one wrapped key
7//! without breaking the other direction's validation.
8
9use bitwarden_api_api::models::V2UpgradeTokenResponseModel;
10use bitwarden_crypto::{Decryptable, EncString, KeyIds, KeyStoreContext, SymmetricKeyAlgorithm};
11use thiserror::Error;
12use tracing::instrument;
13
14/// Holds both V1 and V2 user keys, each wrapped by the other.
15#[cfg_attr(
16    feature = "wasm",
17    derive(tsify::Tsify),
18    tsify(into_wasm_abi, from_wasm_abi)
19)]
20#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
21#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
22pub struct V2UpgradeToken {
23    /// V1 user key encrypted with V2 key (Cose_Encrypt0_B64 format)
24    pub wrapped_user_key_1: EncString,
25    /// V2 user key encrypted with V1 key (Aes256Cbc_HmacSha256_B64 format)
26    pub wrapped_user_key_2: EncString,
27}
28
29impl V2UpgradeToken {
30    /// Creates a new [`V2UpgradeToken`] from `v1_key_id` (Aes256CbcHmac) and `v2_key_id`
31    /// (XChaCha20Poly1305) in the KeyStore. Type-checks both keys, then wraps V1 with V2 and
32    /// V2 with V1.
33    #[instrument(skip(ctx))]
34    pub fn create<Ids: KeyIds>(
35        v1_key_id: Ids::Symmetric,
36        v2_key_id: Ids::Symmetric,
37        ctx: &KeyStoreContext<Ids>,
38    ) -> Result<Self, V2UpgradeTokenError> {
39        // Type-check the keys
40        if ctx
41            .get_symmetric_key_algorithm(v1_key_id)
42            .map_err(|_| V2UpgradeTokenError::KeyMissing)?
43            != SymmetricKeyAlgorithm::Aes256CbcHmac
44        {
45            return Err(V2UpgradeTokenError::WrongKeyType);
46        }
47
48        if ctx
49            .get_symmetric_key_algorithm(v2_key_id)
50            .map_err(|_| V2UpgradeTokenError::KeyMissing)?
51            != SymmetricKeyAlgorithm::XChaCha20Poly1305
52        {
53            return Err(V2UpgradeTokenError::WrongKeyType);
54        }
55
56        // Wrap V1 key with V2 key
57        let wrapped_user_key_1 = ctx
58            .wrap_symmetric_key(v2_key_id, v1_key_id)
59            .map_err(|_| V2UpgradeTokenError::EncryptionFailed)?;
60
61        // Wrap V2 key with V1 key
62        let wrapped_user_key_2 = ctx
63            .wrap_symmetric_key(v1_key_id, v2_key_id)
64            .map_err(|_| V2UpgradeTokenError::EncryptionFailed)?;
65
66        Ok(V2UpgradeToken {
67            wrapped_user_key_1,
68            wrapped_user_key_2,
69        })
70    }
71
72    /// Unwraps `wrapped_user_key_1` using `v2_key_id`, validates the result can unwrap
73    /// `wrapped_user_key_2`, then adds the V1 key to the KeyStore and returns its key ID.
74    #[instrument(skip(self, ctx))]
75    pub fn unwrap_v1<Ids: KeyIds>(
76        &self,
77        v2_key_id: Ids::Symmetric,
78        ctx: &mut KeyStoreContext<Ids>,
79    ) -> Result<Ids::Symmetric, V2UpgradeTokenError> {
80        // Decrypt wrapped V1 key with V2 key and add V1 key to the store
81        let v1_key_id = ctx
82            .unwrap_symmetric_key(v2_key_id, &self.wrapped_user_key_1)
83            .map_err(|_| V2UpgradeTokenError::DecryptionFailed)?;
84
85        // Validate: unwrapped V1 should be able to decrypt wrapped V2 key
86        let _: Vec<u8> = self
87            .wrapped_user_key_2
88            .decrypt(ctx, v1_key_id)
89            .map_err(|_| V2UpgradeTokenError::ValidationFailed)?;
90
91        Ok(v1_key_id)
92    }
93
94    /// Unwraps `wrapped_user_key_2` using `v1_key_id`, validates the result can unwrap
95    /// `wrapped_user_key_1`, then adds the V2 key to the KeyStore and returns its key ID.
96    #[instrument(skip(self, ctx))]
97    pub fn unwrap_v2<Ids: KeyIds>(
98        &self,
99        v1_key_id: Ids::Symmetric,
100        ctx: &mut KeyStoreContext<Ids>,
101    ) -> Result<Ids::Symmetric, V2UpgradeTokenError> {
102        // Decrypt rapped V2 key with V1 key and add V2 key to the store
103        let v2_key_id = ctx
104            .unwrap_symmetric_key(v1_key_id, &self.wrapped_user_key_2)
105            .map_err(|_| V2UpgradeTokenError::DecryptionFailed)?;
106
107        // Validate: unwrapped V2 should be able to decrypt wrapped V1 key
108        let _: Vec<u8> = self
109            .wrapped_user_key_1
110            .decrypt(ctx, v2_key_id)
111            .map_err(|_| V2UpgradeTokenError::ValidationFailed)?;
112
113        Ok(v2_key_id)
114    }
115}
116
117impl TryFrom<&V2UpgradeTokenResponseModel> for V2UpgradeToken {
118    type Error = V2UpgradeTokenError;
119
120    fn try_from(response: &V2UpgradeTokenResponseModel) -> Result<Self, Self::Error> {
121        let wrapped_user_key_1 = response
122            .wrapped_user_key1
123            .as_deref()
124            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
125            .parse()
126            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
127
128        let wrapped_user_key_2 = response
129            .wrapped_user_key2
130            .as_deref()
131            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
132            .parse()
133            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
134
135        Ok(V2UpgradeToken {
136            wrapped_user_key_1,
137            wrapped_user_key_2,
138        })
139    }
140}
141
142/// Errors that can occur when working with V2UpgradeToken
143#[derive(Debug, Error)]
144pub enum V2UpgradeTokenError {
145    /// Decryption of a wrapped key failed
146    #[error("Decryption failed")]
147    DecryptionFailed,
148    /// Bidirectional validation failed - token may be tampered with
149    #[error("Validation failed")]
150    ValidationFailed,
151    /// Serialization or deserialization failed
152    #[error("Serialization error")]
153    Serialization,
154    /// Wrong key type provided (expected V1 or V2)
155    #[error("Wrong key type")]
156    WrongKeyType,
157    /// Key not found in KeyStore
158    #[error("Key missing")]
159    KeyMissing,
160    /// Failed to encrypt a key
161    #[error("Encryption failed")]
162    EncryptionFailed,
163    /// Response model is malformed (missing or unparseable fields)
164    #[error("Response model malformed")]
165    ResponseModelMalformed,
166}
167
168#[cfg(test)]
169mod tests {
170    use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
171
172    use super::*;
173    use crate::key_management::KeyIds;
174
175    #[test]
176    fn test_create_and_round_trip() {
177        let key_store = KeyStore::<KeyIds>::default();
178        let mut ctx = key_store.context_mut();
179
180        // Create V1 and V2 keys
181        let v1_key_id = ctx.generate_symmetric_key();
182        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
183
184        // Create token
185        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
186            .expect("Token creation should succeed");
187
188        // Serialize and deserialize
189        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
190        let deserialized: V2UpgradeToken =
191            serde_json::from_str(&serialized).expect("Deserialization should succeed");
192
193        // Unwrap V2 using V1
194        let unwrapped_v2_id = deserialized
195            .unwrap_v2(v1_key_id, &mut ctx)
196            .expect("Unwrapping V2 should succeed");
197
198        // Verify the unwrapped V2 key matches original
199        #[allow(deprecated)]
200        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
201        #[allow(deprecated)]
202        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
203        assert_eq!(original_v2, unwrapped_v2);
204    }
205
206    #[test]
207    fn test_unwrap_bidirectional() {
208        let key_store = KeyStore::<KeyIds>::default();
209        let mut ctx = key_store.context_mut();
210
211        // Create V1 and V2 keys
212        let v1_key_id = ctx.generate_symmetric_key();
213        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
214
215        // Create token
216        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
217            .expect("Token creation should succeed");
218
219        // Unwrap V2 using V1
220        let unwrapped_v2_id = token
221            .unwrap_v2(v1_key_id, &mut ctx)
222            .expect("Unwrapping V2 should succeed");
223
224        // Unwrap V1 using the unwrapped V2
225        let unwrapped_v1_id = token
226            .unwrap_v1(unwrapped_v2_id, &mut ctx)
227            .expect("Unwrapping V1 should succeed");
228
229        // Verify both unwrapped keys match originals
230        #[allow(deprecated)]
231        let original_v1 = ctx.dangerous_get_symmetric_key(v1_key_id).unwrap();
232        #[allow(deprecated)]
233        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
234        #[allow(deprecated)]
235        let unwrapped_v1 = ctx.dangerous_get_symmetric_key(unwrapped_v1_id).unwrap();
236        #[allow(deprecated)]
237        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
238
239        assert_eq!(original_v1, unwrapped_v1);
240        assert_eq!(original_v2, unwrapped_v2);
241    }
242
243    #[test]
244    fn test_create_wrong_key_type_error() {
245        let key_store = KeyStore::<KeyIds>::default();
246        let mut ctx = key_store.context_mut();
247
248        // Try to create token with two V1 keys
249        let v1_key_1 = ctx.generate_symmetric_key();
250        let v1_key_2 = ctx.generate_symmetric_key();
251
252        let result = V2UpgradeToken::create(v1_key_1, v1_key_2, &ctx);
253        assert!(matches!(result, Err(V2UpgradeTokenError::WrongKeyType)));
254    }
255
256    #[test]
257    fn test_serialization_round_trip() {
258        let key_store = KeyStore::<KeyIds>::default();
259        let mut ctx = key_store.context_mut();
260
261        let v1_key_id = ctx.generate_symmetric_key();
262        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
263
264        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
265            .expect("Token creation should succeed");
266
267        // Verify serialization produces a JSON object with the expected fields
268        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
269        let json: serde_json::Value =
270            serde_json::from_str(&serialized).expect("Should be valid JSON");
271        assert!(json.is_object());
272        assert!(json.get("wrapped_user_key_1").is_some());
273        assert!(json.get("wrapped_user_key_2").is_some());
274
275        // Verify round-trip: deserialize and re-serialize produces identical output
276        let deserialized: V2UpgradeToken =
277            serde_json::from_str(&serialized).expect("Deserialization should succeed");
278        let reserialized =
279            serde_json::to_string(&deserialized).expect("Reserialization should succeed");
280        assert_eq!(serialized, reserialized);
281    }
282
283    fn build_response_model<Ids: bitwarden_crypto::KeyIds>(
284        v1_key_id: Ids::Symmetric,
285        v2_key_id: Ids::Symmetric,
286        ctx: &KeyStoreContext<Ids>,
287    ) -> V2UpgradeTokenResponseModel {
288        let wrapped_user_key_1 = ctx.wrap_symmetric_key(v2_key_id, v1_key_id).unwrap();
289        let wrapped_user_key_2 = ctx.wrap_symmetric_key(v1_key_id, v2_key_id).unwrap();
290        V2UpgradeTokenResponseModel {
291            wrapped_user_key1: Some(wrapped_user_key_1.to_string()),
292            wrapped_user_key2: Some(wrapped_user_key_2.to_string()),
293        }
294    }
295
296    #[test]
297    fn test_from_response_model_missing_wrapped_uk1() {
298        let response = V2UpgradeTokenResponseModel {
299            wrapped_user_key1: None,
300            wrapped_user_key2: None,
301        };
302        assert!(matches!(
303            V2UpgradeToken::try_from(&response),
304            Err(V2UpgradeTokenError::ResponseModelMalformed)
305        ));
306    }
307
308    #[test]
309    fn test_from_response_model_missing_wrapped_uk2() {
310        let key_store = KeyStore::<KeyIds>::default();
311        let mut ctx = key_store.context_mut();
312
313        let v1_key_id = ctx.generate_symmetric_key();
314        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
315
316        let mut response = build_response_model(v1_key_id, v2_key_id, &ctx);
317        response.wrapped_user_key2 = None;
318
319        assert!(matches!(
320            V2UpgradeToken::try_from(&response),
321            Err(V2UpgradeTokenError::ResponseModelMalformed)
322        ));
323    }
324
325    #[test]
326    fn test_serde_round_trip() {
327        let key_store = KeyStore::<KeyIds>::default();
328        let mut ctx = key_store.context_mut();
329
330        let v1_key_id = ctx.generate_symmetric_key();
331        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
332
333        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
334            .expect("Token creation should succeed");
335
336        // Serialize via serde — produces a JSON object
337        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
338
339        // Deserialize back and verify the token is still functional
340        let deserialized: V2UpgradeToken =
341            serde_json::from_str(&serialized).expect("Deserialization should succeed");
342        let unwrapped_v2_id = deserialized
343            .unwrap_v2(v1_key_id, &mut ctx)
344            .expect("Unwrapping V2 from serde-deserialized token should succeed");
345
346        #[allow(deprecated)]
347        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
348        #[allow(deprecated)]
349        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
350        assert_eq!(original_v2, unwrapped_v2);
351    }
352}