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    /// (XChaCha20Poly1305) in the KeyStore. Type-checks both keys, then wraps V1 with V2 and
42    /// 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 ctx
59            .get_symmetric_key_algorithm(v2_key_id)
60            .map_err(|_| V2UpgradeTokenError::KeyMissing)?
61            != SymmetricKeyAlgorithm::XChaCha20Poly1305
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        // Validate: unwrapped V2 should be able to decrypt wrapped V1 key
118        let _: Vec<u8> = self
119            .wrapped_user_key_1
120            .decrypt(ctx, v2_key_id)
121            .map_err(|_| V2UpgradeTokenError::ValidationFailed)?;
122
123        Ok(v2_key_id)
124    }
125}
126
127impl TryFrom<&V2UpgradeTokenResponseModel> for V2UpgradeToken {
128    type Error = V2UpgradeTokenError;
129
130    fn try_from(response: &V2UpgradeTokenResponseModel) -> Result<Self, Self::Error> {
131        let wrapped_user_key_1 = response
132            .wrapped_user_key1
133            .as_deref()
134            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
135            .parse()
136            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
137
138        let wrapped_user_key_2 = response
139            .wrapped_user_key2
140            .as_deref()
141            .ok_or(V2UpgradeTokenError::ResponseModelMalformed)?
142            .parse()
143            .map_err(|_| V2UpgradeTokenError::ResponseModelMalformed)?;
144
145        Ok(V2UpgradeToken {
146            wrapped_user_key_1,
147            wrapped_user_key_2,
148        })
149    }
150}
151
152impl From<V2UpgradeToken> for V2UpgradeTokenRequestModel {
153    fn from(token: V2UpgradeToken) -> Self {
154        V2UpgradeTokenRequestModel {
155            wrapped_user_key1: token.wrapped_user_key_1.to_string(),
156            wrapped_user_key2: token.wrapped_user_key_2.to_string(),
157        }
158    }
159}
160
161impl TryFrom<V2UpgradeTokenRequestModel> for V2UpgradeToken {
162    type Error = V2UpgradeTokenError;
163
164    fn try_from(request: V2UpgradeTokenRequestModel) -> Result<Self, Self::Error> {
165        let wrapped_user_key_1 = request
166            .wrapped_user_key1
167            .parse()
168            .map_err(|_| V2UpgradeTokenError::RequestMalformed)?;
169        let wrapped_user_key_2 = request
170            .wrapped_user_key2
171            .parse()
172            .map_err(|_| V2UpgradeTokenError::RequestMalformed)?;
173
174        Ok(V2UpgradeToken {
175            wrapped_user_key_1,
176            wrapped_user_key_2,
177        })
178    }
179}
180
181/// Errors that can occur when working with V2UpgradeToken
182#[derive(Debug, Error)]
183pub enum V2UpgradeTokenError {
184    /// Decryption of a wrapped key failed
185    #[error("Decryption failed")]
186    DecryptionFailed,
187    /// Bidirectional validation failed - token may be tampered with
188    #[error("Validation failed")]
189    ValidationFailed,
190    /// Serialization or deserialization failed
191    #[error("Serialization error")]
192    Serialization,
193    /// Wrong key type provided (expected V1 or V2)
194    #[error("Wrong key type")]
195    WrongKeyType,
196    /// Key not found in KeyStore
197    #[error("Key missing")]
198    KeyMissing,
199    /// Failed to encrypt a key
200    #[error("Encryption failed")]
201    EncryptionFailed,
202    /// Response model is malformed (missing or unparseable fields)
203    #[error("Response model malformed")]
204    ResponseModelMalformed,
205    /// Request model is malformed (missing or unparseable fields)
206    #[error("Request model malformed")]
207    RequestMalformed,
208}
209
210#[cfg(test)]
211mod tests {
212    use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
213
214    use super::*;
215    use crate::key_management::KeySlotIds;
216
217    #[test]
218    fn test_create_and_round_trip() {
219        let key_store = KeyStore::<KeySlotIds>::default();
220        let mut ctx = key_store.context_mut();
221
222        // Create V1 and V2 keys
223        let v1_key_id = ctx.generate_symmetric_key();
224        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
225
226        // Create token
227        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
228            .expect("Token creation should succeed");
229
230        // Serialize and deserialize
231        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
232        let deserialized: V2UpgradeToken =
233            serde_json::from_str(&serialized).expect("Deserialization should succeed");
234
235        // Unwrap V2 using V1
236        let unwrapped_v2_id = deserialized
237            .unwrap_v2(v1_key_id, &mut ctx)
238            .expect("Unwrapping V2 should succeed");
239
240        // Verify the unwrapped V2 key matches original
241        #[allow(deprecated)]
242        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
243        #[allow(deprecated)]
244        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
245        assert_eq!(original_v2, unwrapped_v2);
246    }
247
248    #[test]
249    fn test_unwrap_bidirectional() {
250        let key_store = KeyStore::<KeySlotIds>::default();
251        let mut ctx = key_store.context_mut();
252
253        // Create V1 and V2 keys
254        let v1_key_id = ctx.generate_symmetric_key();
255        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
256
257        // Create token
258        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
259            .expect("Token creation should succeed");
260
261        // Unwrap V2 using V1
262        let unwrapped_v2_id = token
263            .unwrap_v2(v1_key_id, &mut ctx)
264            .expect("Unwrapping V2 should succeed");
265
266        // Unwrap V1 using the unwrapped V2
267        let unwrapped_v1_id = token
268            .unwrap_v1(unwrapped_v2_id, &mut ctx)
269            .expect("Unwrapping V1 should succeed");
270
271        // Verify both unwrapped keys match originals
272        #[allow(deprecated)]
273        let original_v1 = ctx.dangerous_get_symmetric_key(v1_key_id).unwrap();
274        #[allow(deprecated)]
275        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
276        #[allow(deprecated)]
277        let unwrapped_v1 = ctx.dangerous_get_symmetric_key(unwrapped_v1_id).unwrap();
278        #[allow(deprecated)]
279        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
280
281        assert_eq!(original_v1, unwrapped_v1);
282        assert_eq!(original_v2, unwrapped_v2);
283    }
284
285    #[test]
286    fn test_create_wrong_key_type_error() {
287        let key_store = KeyStore::<KeySlotIds>::default();
288        let mut ctx = key_store.context_mut();
289
290        // Try to create token with two V1 keys
291        let v1_key_1 = ctx.generate_symmetric_key();
292        let v1_key_2 = ctx.generate_symmetric_key();
293
294        let result = V2UpgradeToken::create(v1_key_1, v1_key_2, &ctx);
295        assert!(matches!(result, Err(V2UpgradeTokenError::WrongKeyType)));
296    }
297
298    #[test]
299    fn test_serialization_round_trip() {
300        let key_store = KeyStore::<KeySlotIds>::default();
301        let mut ctx = key_store.context_mut();
302
303        let v1_key_id = ctx.generate_symmetric_key();
304        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
305
306        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
307            .expect("Token creation should succeed");
308
309        // Verify serialization produces a JSON object with the expected fields
310        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
311        let json: serde_json::Value =
312            serde_json::from_str(&serialized).expect("Should be valid JSON");
313        assert!(json.is_object());
314        assert!(json.get("wrapped_user_key_1").is_some());
315        assert!(json.get("wrapped_user_key_2").is_some());
316
317        // Verify round-trip: deserialize and re-serialize produces identical output
318        let deserialized: V2UpgradeToken =
319            serde_json::from_str(&serialized).expect("Deserialization should succeed");
320        let reserialized =
321            serde_json::to_string(&deserialized).expect("Reserialization should succeed");
322        assert_eq!(serialized, reserialized);
323    }
324
325    fn build_response_model<Ids: bitwarden_crypto::KeySlotIds>(
326        v1_key_id: Ids::Symmetric,
327        v2_key_id: Ids::Symmetric,
328        ctx: &KeyStoreContext<Ids>,
329    ) -> V2UpgradeTokenResponseModel {
330        let wrapped_user_key_1 = ctx.wrap_symmetric_key(v2_key_id, v1_key_id).unwrap();
331        let wrapped_user_key_2 = ctx.wrap_symmetric_key(v1_key_id, v2_key_id).unwrap();
332        V2UpgradeTokenResponseModel {
333            wrapped_user_key1: Some(wrapped_user_key_1.to_string()),
334            wrapped_user_key2: Some(wrapped_user_key_2.to_string()),
335        }
336    }
337
338    #[test]
339    fn test_from_response_model_missing_wrapped_uk1() {
340        let response = V2UpgradeTokenResponseModel {
341            wrapped_user_key1: None,
342            wrapped_user_key2: None,
343        };
344        assert!(matches!(
345            V2UpgradeToken::try_from(&response),
346            Err(V2UpgradeTokenError::ResponseModelMalformed)
347        ));
348    }
349
350    #[test]
351    fn test_from_response_model_missing_wrapped_uk2() {
352        let key_store = KeyStore::<KeySlotIds>::default();
353        let mut ctx = key_store.context_mut();
354
355        let v1_key_id = ctx.generate_symmetric_key();
356        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
357
358        let mut response = build_response_model(v1_key_id, v2_key_id, &ctx);
359        response.wrapped_user_key2 = None;
360
361        assert!(matches!(
362            V2UpgradeToken::try_from(&response),
363            Err(V2UpgradeTokenError::ResponseModelMalformed)
364        ));
365    }
366
367    #[test]
368    fn test_serde_round_trip() {
369        let key_store = KeyStore::<KeySlotIds>::default();
370        let mut ctx = key_store.context_mut();
371
372        let v1_key_id = ctx.generate_symmetric_key();
373        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
374
375        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
376            .expect("Token creation should succeed");
377
378        // Serialize via serde — produces a JSON object
379        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
380
381        // Deserialize back and verify the token is still functional
382        let deserialized: V2UpgradeToken =
383            serde_json::from_str(&serialized).expect("Deserialization should succeed");
384        let unwrapped_v2_id = deserialized
385            .unwrap_v2(v1_key_id, &mut ctx)
386            .expect("Unwrapping V2 from serde-deserialized token should succeed");
387
388        #[allow(deprecated)]
389        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
390        #[allow(deprecated)]
391        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
392        assert_eq!(original_v2, unwrapped_v2);
393    }
394
395    #[test]
396    fn test_from_token_to_request_model() {
397        let key_store = KeyStore::<KeySlotIds>::default();
398        let mut ctx = key_store.context_mut();
399
400        let v1_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
401        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
402
403        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
404            .expect("Token creation should succeed");
405
406        let expected_wrapped_key1 = token.wrapped_user_key_1.to_string();
407        let expected_wrapped_key2 = token.wrapped_user_key_2.to_string();
408
409        let request_model: V2UpgradeTokenRequestModel = token.into();
410
411        assert_eq!(request_model.wrapped_user_key1, expected_wrapped_key1);
412        assert_eq!(request_model.wrapped_user_key2, expected_wrapped_key2);
413
414        request_model
415            .wrapped_user_key1
416            .parse::<EncString>()
417            .expect("wrapped_user_key1 should be a valid EncString");
418        request_model
419            .wrapped_user_key2
420            .parse::<EncString>()
421            .expect("wrapped_user_key2 should be a valid EncString");
422    }
423}