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