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