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