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::{V2UpgradeTokenRequestModel, V2UpgradeTokenResponseModel};
10use bitwarden_crypto::{
11    Decryptable, EncString, KeySlotIds, KeyStoreContext, SymmetricKeyAlgorithm,
12};
13use thiserror::Error;
14
15/// Holds both V1 and V2 user keys, each wrapped by the other.
16#[bitwarden_ffi::wasm_record]
17#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
18#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
19pub struct V2UpgradeToken {
20    /// V1 user key encrypted with V2 key (Cose_Encrypt0_B64 format)
21    pub wrapped_user_key_1: EncString,
22    /// V2 user key encrypted with V1 key (Aes256Cbc_HmacSha256_B64 format)
23    pub wrapped_user_key_2: EncString,
24}
25
26#[cfg(feature = "wasm")]
27impl TryFrom<wasm_bindgen::JsValue> for V2UpgradeToken {
28    type Error = serde_wasm_bindgen::Error;
29
30    fn try_from(value: wasm_bindgen::JsValue) -> Result<Self, Self::Error> {
31        serde_wasm_bindgen::from_value(value)
32    }
33}
34
35impl V2UpgradeToken {
36    /// Creates a new [`V2UpgradeToken`] from `v1_key_id` (Aes256CbcHmac) and `v2_key_id`
37    /// (XAES-256-GCM) in the KeyStore. Type-checks both keys, then wraps V1
38    /// with V2 and V2 with V1.
39    #[bitwarden_logging::instrument(fields(v1_key_id = ?v1_key_id, v2_key_id = ?v2_key_id))]
40    pub fn create<Ids: KeySlotIds>(
41        v1_key_id: Ids::Symmetric,
42        v2_key_id: Ids::Symmetric,
43        ctx: &KeyStoreContext<Ids>,
44    ) -> Result<Self, V2UpgradeTokenError> {
45        // Type-check the keys
46        if ctx
47            .get_symmetric_key_algorithm(v1_key_id)
48            .map_err(|_| V2UpgradeTokenError::KeyMissing)?
49            != SymmetricKeyAlgorithm::Aes256CbcHmac
50        {
51            return Err(V2UpgradeTokenError::WrongKeyType);
52        }
53
54        if !matches!(
55            ctx.get_symmetric_key_algorithm(v2_key_id)
56                .map_err(|_| V2UpgradeTokenError::KeyMissing)?,
57            SymmetricKeyAlgorithm::XAes256Gcm
58        ) {
59            return Err(V2UpgradeTokenError::WrongKeyType);
60        }
61
62        // Wrap V1 key with V2 key
63        let wrapped_user_key_1 = ctx
64            .wrap_symmetric_key(v2_key_id, v1_key_id)
65            .map_err(|_| V2UpgradeTokenError::EncryptionFailed)?;
66
67        // Wrap V2 key with V1 key
68        let wrapped_user_key_2 = ctx
69            .wrap_symmetric_key(v1_key_id, v2_key_id)
70            .map_err(|_| V2UpgradeTokenError::EncryptionFailed)?;
71
72        Ok(V2UpgradeToken {
73            wrapped_user_key_1,
74            wrapped_user_key_2,
75        })
76    }
77
78    /// Unwraps `wrapped_user_key_1` using `v2_key_id`, validates the result can unwrap
79    /// `wrapped_user_key_2`, then adds the V1 key to the KeyStore and returns its key ID.
80    #[bitwarden_logging::instrument(fields(v2_key_id = ?v2_key_id))]
81    pub fn unwrap_v1<Ids: KeySlotIds>(
82        &self,
83        v2_key_id: Ids::Symmetric,
84        ctx: &mut KeyStoreContext<Ids>,
85    ) -> Result<Ids::Symmetric, V2UpgradeTokenError> {
86        // Decrypt wrapped V1 key with V2 key and add V1 key to the store
87        let v1_key_id = ctx
88            .unwrap_symmetric_key(v2_key_id, &self.wrapped_user_key_1)
89            .map_err(|_| V2UpgradeTokenError::DecryptionFailed)?;
90
91        // Validate: unwrapped V1 should be able to decrypt wrapped V2 key
92        let _: Vec<u8> = self
93            .wrapped_user_key_2
94            .decrypt(ctx, v1_key_id)
95            .map_err(|_| V2UpgradeTokenError::ValidationFailed)?;
96
97        Ok(v1_key_id)
98    }
99
100    /// Unwraps `wrapped_user_key_2` using `v1_key_id`, validates the result can unwrap
101    /// `wrapped_user_key_1`, then adds the V2 key to the KeyStore and returns its key ID.
102    #[bitwarden_logging::instrument(fields(v1_key_id = ?v1_key_id))]
103    pub fn unwrap_v2<Ids: KeySlotIds>(
104        &self,
105        v1_key_id: Ids::Symmetric,
106        ctx: &mut KeyStoreContext<Ids>,
107    ) -> Result<Ids::Symmetric, V2UpgradeTokenError> {
108        // Decrypt rapped V2 key with V1 key and add V2 key to the store
109        let v2_key_id = ctx
110            .unwrap_symmetric_key(v1_key_id, &self.wrapped_user_key_2)
111            .map_err(|_| V2UpgradeTokenError::DecryptionFailed)?;
112
113        if ctx
114            .get_symmetric_key_algorithm(v2_key_id)
115            .map_err(|_| V2UpgradeTokenError::KeyMissing)?
116            != SymmetricKeyAlgorithm::XAes256Gcm
117        {
118            return Err(V2UpgradeTokenError::WrongKeyType);
119        }
120
121        // Validate: unwrapped V2 should be able to decrypt wrapped V1 key
122        let _: Vec<u8> = self
123            .wrapped_user_key_1
124            .decrypt(ctx, v2_key_id)
125            .map_err(|_| V2UpgradeTokenError::ValidationFailed)?;
126
127        Ok(v2_key_id)
128    }
129}
130
131impl TryFrom<&V2UpgradeTokenResponseModel> for V2UpgradeToken {
132    type Error = V2UpgradeTokenError;
133
134    fn try_from(response: &V2UpgradeTokenResponseModel) -> Result<Self, Self::Error> {
135        let wrapped_user_key_1 = response
136            .wrapped_user_key1
137            .as_deref()
138            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
139            .parse()
140            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
141
142        let wrapped_user_key_2 = response
143            .wrapped_user_key2
144            .as_deref()
145            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
146            .parse()
147            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
148
149        Ok(V2UpgradeToken {
150            wrapped_user_key_1,
151            wrapped_user_key_2,
152        })
153    }
154}
155
156impl From<V2UpgradeToken> for V2UpgradeTokenRequestModel {
157    fn from(token: V2UpgradeToken) -> Self {
158        V2UpgradeTokenRequestModel {
159            wrapped_user_key1: token.wrapped_user_key_1.to_string(),
160            wrapped_user_key2: token.wrapped_user_key_2.to_string(),
161        }
162    }
163}
164
165impl TryFrom<V2UpgradeTokenRequestModel> for V2UpgradeToken {
166    type Error = V2UpgradeTokenError;
167
168    fn try_from(request: V2UpgradeTokenRequestModel) -> Result<Self, Self::Error> {
169        let wrapped_user_key_1 = request
170            .wrapped_user_key1
171            .parse()
172            .map_err(|_| V2UpgradeTokenError::RequestMalformed)?;
173        let wrapped_user_key_2 = request
174            .wrapped_user_key2
175            .parse()
176            .map_err(|_| V2UpgradeTokenError::RequestMalformed)?;
177
178        Ok(V2UpgradeToken {
179            wrapped_user_key_1,
180            wrapped_user_key_2,
181        })
182    }
183}
184
185/// Errors that can occur when working with V2UpgradeToken
186#[derive(Debug, Error)]
187pub enum V2UpgradeTokenError {
188    /// Decryption of a wrapped key failed
189    #[error("Decryption failed")]
190    DecryptionFailed,
191    /// Bidirectional validation failed - token may be tampered with
192    #[error("Validation failed")]
193    ValidationFailed,
194    /// Serialization or deserialization failed
195    #[error("Serialization error")]
196    Serialization,
197    /// Wrong key type provided (expected V1 or V2)
198    #[error("Wrong key type")]
199    WrongKeyType,
200    /// Key not found in KeyStore
201    #[error("Key missing")]
202    KeyMissing,
203    /// Failed to encrypt a key
204    #[error("Encryption failed")]
205    EncryptionFailed,
206    /// Response model is malformed (missing or unparseable fields)
207    #[error("Response model malformed")]
208    ResponseModelMalformed,
209    /// Request model is malformed (missing or unparseable fields)
210    #[error("Request model malformed")]
211    RequestMalformed,
212}
213
214#[cfg(test)]
215mod tests {
216    use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
217
218    use super::*;
219    use crate::key_management::KeySlotIds;
220
221    #[test]
222    fn test_create_and_round_trip() {
223        let key_store = KeyStore::<KeySlotIds>::default();
224        let mut ctx = key_store.context_mut();
225
226        // Create V1 and V2 keys
227        let v1_key_id = ctx.generate_symmetric_key();
228        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
229
230        // Create token
231        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
232            .expect("Token creation should succeed");
233
234        // Serialize and deserialize
235        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
236        let deserialized: V2UpgradeToken =
237            serde_json::from_str(&serialized).expect("Deserialization should succeed");
238
239        // Unwrap V2 using V1
240        let unwrapped_v2_id = deserialized
241            .unwrap_v2(v1_key_id, &mut ctx)
242            .expect("Unwrapping V2 should succeed");
243
244        // Verify the unwrapped V2 key matches original
245        #[allow(deprecated)]
246        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
247        #[allow(deprecated)]
248        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
249        assert_eq!(original_v2, unwrapped_v2);
250    }
251
252    #[test]
253    fn test_unwrap_bidirectional() {
254        let key_store = KeyStore::<KeySlotIds>::default();
255        let mut ctx = key_store.context_mut();
256
257        // Create V1 and V2 keys
258        let v1_key_id = ctx.generate_symmetric_key();
259        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
260
261        // Create token
262        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
263            .expect("Token creation should succeed");
264
265        // Unwrap V2 using V1
266        let unwrapped_v2_id = token
267            .unwrap_v2(v1_key_id, &mut ctx)
268            .expect("Unwrapping V2 should succeed");
269
270        // Unwrap V1 using the unwrapped V2
271        let unwrapped_v1_id = token
272            .unwrap_v1(unwrapped_v2_id, &mut ctx)
273            .expect("Unwrapping V1 should succeed");
274
275        // Verify both unwrapped keys match originals
276        #[allow(deprecated)]
277        let original_v1 = ctx.dangerous_get_symmetric_key(v1_key_id).unwrap();
278        #[allow(deprecated)]
279        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
280        #[allow(deprecated)]
281        let unwrapped_v1 = ctx.dangerous_get_symmetric_key(unwrapped_v1_id).unwrap();
282        #[allow(deprecated)]
283        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
284
285        assert_eq!(original_v1, unwrapped_v1);
286        assert_eq!(original_v2, unwrapped_v2);
287    }
288
289    #[test]
290    fn test_create_wrong_key_type_error() {
291        let key_store = KeyStore::<KeySlotIds>::default();
292        let mut ctx = key_store.context_mut();
293
294        // Try to create token with two V1 keys
295        let v1_key_1 = ctx.generate_symmetric_key();
296        let v1_key_2 = ctx.generate_symmetric_key();
297
298        let result = V2UpgradeToken::create(v1_key_1, v1_key_2, &ctx);
299        assert!(matches!(result, Err(V2UpgradeTokenError::WrongKeyType)));
300    }
301
302    #[test]
303    fn test_serialization_round_trip() {
304        let key_store = KeyStore::<KeySlotIds>::default();
305        let mut ctx = key_store.context_mut();
306
307        let v1_key_id = ctx.generate_symmetric_key();
308        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
309
310        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
311            .expect("Token creation should succeed");
312
313        // Verify serialization produces a JSON object with the expected fields
314        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
315        let json: serde_json::Value =
316            serde_json::from_str(&serialized).expect("Should be valid JSON");
317        assert!(json.is_object());
318        assert!(json.get("wrapped_user_key_1").is_some());
319        assert!(json.get("wrapped_user_key_2").is_some());
320
321        // Verify round-trip: deserialize and re-serialize produces identical output
322        let deserialized: V2UpgradeToken =
323            serde_json::from_str(&serialized).expect("Deserialization should succeed");
324        let reserialized =
325            serde_json::to_string(&deserialized).expect("Reserialization should succeed");
326        assert_eq!(serialized, reserialized);
327    }
328
329    fn build_response_model<Ids: bitwarden_crypto::KeySlotIds>(
330        v1_key_id: Ids::Symmetric,
331        v2_key_id: Ids::Symmetric,
332        ctx: &KeyStoreContext<Ids>,
333    ) -> V2UpgradeTokenResponseModel {
334        let wrapped_user_key_1 = ctx.wrap_symmetric_key(v2_key_id, v1_key_id).unwrap();
335        let wrapped_user_key_2 = ctx.wrap_symmetric_key(v1_key_id, v2_key_id).unwrap();
336        V2UpgradeTokenResponseModel {
337            wrapped_user_key1: Some(wrapped_user_key_1.to_string()),
338            wrapped_user_key2: Some(wrapped_user_key_2.to_string()),
339        }
340    }
341
342    #[test]
343    fn test_from_response_model_missing_wrapped_uk1() {
344        let response = V2UpgradeTokenResponseModel {
345            wrapped_user_key1: None,
346            wrapped_user_key2: None,
347        };
348        assert!(matches!(
349            V2UpgradeToken::try_from(&response),
350            Err(V2UpgradeTokenError::ResponseModelMalformed)
351        ));
352    }
353
354    #[test]
355    fn test_from_response_model_missing_wrapped_uk2() {
356        let key_store = KeyStore::<KeySlotIds>::default();
357        let mut ctx = key_store.context_mut();
358
359        let v1_key_id = ctx.generate_symmetric_key();
360        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
361
362        let mut response = build_response_model(v1_key_id, v2_key_id, &ctx);
363        response.wrapped_user_key2 = None;
364
365        assert!(matches!(
366            V2UpgradeToken::try_from(&response),
367            Err(V2UpgradeTokenError::ResponseModelMalformed)
368        ));
369    }
370
371    #[test]
372    fn test_serde_round_trip() {
373        let key_store = KeyStore::<KeySlotIds>::default();
374        let mut ctx = key_store.context_mut();
375
376        let v1_key_id = ctx.generate_symmetric_key();
377        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
378
379        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
380            .expect("Token creation should succeed");
381
382        // Serialize via serde — produces a JSON object
383        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
384
385        // Deserialize back and verify the token is still functional
386        let deserialized: V2UpgradeToken =
387            serde_json::from_str(&serialized).expect("Deserialization should succeed");
388        let unwrapped_v2_id = deserialized
389            .unwrap_v2(v1_key_id, &mut ctx)
390            .expect("Unwrapping V2 from serde-deserialized token should succeed");
391
392        #[allow(deprecated)]
393        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
394        #[allow(deprecated)]
395        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
396        assert_eq!(original_v2, unwrapped_v2);
397    }
398
399    #[test]
400    fn test_from_token_to_request_model() {
401        let key_store = KeyStore::<KeySlotIds>::default();
402        let mut ctx = key_store.context_mut();
403
404        let v1_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
405        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XAes256Gcm);
406
407        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
408            .expect("Token creation should succeed");
409
410        let expected_wrapped_key1 = token.wrapped_user_key_1.to_string();
411        let expected_wrapped_key2 = token.wrapped_user_key_2.to_string();
412
413        let request_model: V2UpgradeTokenRequestModel = token.into();
414
415        assert_eq!(request_model.wrapped_user_key1, expected_wrapped_key1);
416        assert_eq!(request_model.wrapped_user_key2, expected_wrapped_key2);
417
418        request_model
419            .wrapped_user_key1
420            .parse::<EncString>()
421            .expect("wrapped_user_key1 should be a valid EncString");
422        request_model
423            .wrapped_user_key2
424            .parse::<EncString>()
425            .expect("wrapped_user_key2 should be a valid EncString");
426    }
427}