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::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
144/// Errors that can occur when working with V2UpgradeToken
145#[derive(Debug, Error)]
146pub enum V2UpgradeTokenError {
147    /// Decryption of a wrapped key failed
148    #[error("Decryption failed")]
149    DecryptionFailed,
150    /// Bidirectional validation failed - token may be tampered with
151    #[error("Validation failed")]
152    ValidationFailed,
153    /// Serialization or deserialization failed
154    #[error("Serialization error")]
155    Serialization,
156    /// Wrong key type provided (expected V1 or V2)
157    #[error("Wrong key type")]
158    WrongKeyType,
159    /// Key not found in KeyStore
160    #[error("Key missing")]
161    KeyMissing,
162    /// Failed to encrypt a key
163    #[error("Encryption failed")]
164    EncryptionFailed,
165    /// Response model is malformed (missing or unparseable fields)
166    #[error("Response model malformed")]
167    ResponseModelMalformed,
168}
169
170#[cfg(test)]
171mod tests {
172    use bitwarden_crypto::{KeyStore, SymmetricKeyAlgorithm};
173
174    use super::*;
175    use crate::key_management::KeyIds;
176
177    #[test]
178    fn test_create_and_round_trip() {
179        let key_store = KeyStore::<KeyIds>::default();
180        let mut ctx = key_store.context_mut();
181
182        // Create V1 and V2 keys
183        let v1_key_id = ctx.generate_symmetric_key();
184        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
185
186        // Create token
187        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
188            .expect("Token creation should succeed");
189
190        // Serialize and deserialize
191        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
192        let deserialized: V2UpgradeToken =
193            serde_json::from_str(&serialized).expect("Deserialization should succeed");
194
195        // Unwrap V2 using V1
196        let unwrapped_v2_id = deserialized
197            .unwrap_v2(v1_key_id, &mut ctx)
198            .expect("Unwrapping V2 should succeed");
199
200        // Verify the unwrapped V2 key matches original
201        #[allow(deprecated)]
202        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
203        #[allow(deprecated)]
204        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
205        assert_eq!(original_v2, unwrapped_v2);
206    }
207
208    #[test]
209    fn test_unwrap_bidirectional() {
210        let key_store = KeyStore::<KeyIds>::default();
211        let mut ctx = key_store.context_mut();
212
213        // Create V1 and V2 keys
214        let v1_key_id = ctx.generate_symmetric_key();
215        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
216
217        // Create token
218        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
219            .expect("Token creation should succeed");
220
221        // Unwrap V2 using V1
222        let unwrapped_v2_id = token
223            .unwrap_v2(v1_key_id, &mut ctx)
224            .expect("Unwrapping V2 should succeed");
225
226        // Unwrap V1 using the unwrapped V2
227        let unwrapped_v1_id = token
228            .unwrap_v1(unwrapped_v2_id, &mut ctx)
229            .expect("Unwrapping V1 should succeed");
230
231        // Verify both unwrapped keys match originals
232        #[allow(deprecated)]
233        let original_v1 = ctx.dangerous_get_symmetric_key(v1_key_id).unwrap();
234        #[allow(deprecated)]
235        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
236        #[allow(deprecated)]
237        let unwrapped_v1 = ctx.dangerous_get_symmetric_key(unwrapped_v1_id).unwrap();
238        #[allow(deprecated)]
239        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
240
241        assert_eq!(original_v1, unwrapped_v1);
242        assert_eq!(original_v2, unwrapped_v2);
243    }
244
245    #[test]
246    fn test_create_wrong_key_type_error() {
247        let key_store = KeyStore::<KeyIds>::default();
248        let mut ctx = key_store.context_mut();
249
250        // Try to create token with two V1 keys
251        let v1_key_1 = ctx.generate_symmetric_key();
252        let v1_key_2 = ctx.generate_symmetric_key();
253
254        let result = V2UpgradeToken::create(v1_key_1, v1_key_2, &ctx);
255        assert!(matches!(result, Err(V2UpgradeTokenError::WrongKeyType)));
256    }
257
258    #[test]
259    fn test_serialization_round_trip() {
260        let key_store = KeyStore::<KeyIds>::default();
261        let mut ctx = key_store.context_mut();
262
263        let v1_key_id = ctx.generate_symmetric_key();
264        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
265
266        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
267            .expect("Token creation should succeed");
268
269        // Verify serialization produces a JSON object with the expected fields
270        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
271        let json: serde_json::Value =
272            serde_json::from_str(&serialized).expect("Should be valid JSON");
273        assert!(json.is_object());
274        assert!(json.get("wrapped_user_key_1").is_some());
275        assert!(json.get("wrapped_user_key_2").is_some());
276
277        // Verify round-trip: deserialize and re-serialize produces identical output
278        let deserialized: V2UpgradeToken =
279            serde_json::from_str(&serialized).expect("Deserialization should succeed");
280        let reserialized =
281            serde_json::to_string(&deserialized).expect("Reserialization should succeed");
282        assert_eq!(serialized, reserialized);
283    }
284
285    fn build_response_model<Ids: bitwarden_crypto::KeySlotIds>(
286        v1_key_id: Ids::Symmetric,
287        v2_key_id: Ids::Symmetric,
288        ctx: &KeyStoreContext<Ids>,
289    ) -> V2UpgradeTokenResponseModel {
290        let wrapped_user_key_1 = ctx.wrap_symmetric_key(v2_key_id, v1_key_id).unwrap();
291        let wrapped_user_key_2 = ctx.wrap_symmetric_key(v1_key_id, v2_key_id).unwrap();
292        V2UpgradeTokenResponseModel {
293            wrapped_user_key1: Some(wrapped_user_key_1.to_string()),
294            wrapped_user_key2: Some(wrapped_user_key_2.to_string()),
295        }
296    }
297
298    #[test]
299    fn test_from_response_model_missing_wrapped_uk1() {
300        let response = V2UpgradeTokenResponseModel {
301            wrapped_user_key1: None,
302            wrapped_user_key2: None,
303        };
304        assert!(matches!(
305            V2UpgradeToken::try_from(&response),
306            Err(V2UpgradeTokenError::ResponseModelMalformed)
307        ));
308    }
309
310    #[test]
311    fn test_from_response_model_missing_wrapped_uk2() {
312        let key_store = KeyStore::<KeyIds>::default();
313        let mut ctx = key_store.context_mut();
314
315        let v1_key_id = ctx.generate_symmetric_key();
316        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
317
318        let mut response = build_response_model(v1_key_id, v2_key_id, &ctx);
319        response.wrapped_user_key2 = None;
320
321        assert!(matches!(
322            V2UpgradeToken::try_from(&response),
323            Err(V2UpgradeTokenError::ResponseModelMalformed)
324        ));
325    }
326
327    #[test]
328    fn test_serde_round_trip() {
329        let key_store = KeyStore::<KeyIds>::default();
330        let mut ctx = key_store.context_mut();
331
332        let v1_key_id = ctx.generate_symmetric_key();
333        let v2_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
334
335        let token = V2UpgradeToken::create(v1_key_id, v2_key_id, &ctx)
336            .expect("Token creation should succeed");
337
338        // Serialize via serde — produces a JSON object
339        let serialized = serde_json::to_string(&token).expect("Serialization should succeed");
340
341        // Deserialize back and verify the token is still functional
342        let deserialized: V2UpgradeToken =
343            serde_json::from_str(&serialized).expect("Deserialization should succeed");
344        let unwrapped_v2_id = deserialized
345            .unwrap_v2(v1_key_id, &mut ctx)
346            .expect("Unwrapping V2 from serde-deserialized token should succeed");
347
348        #[allow(deprecated)]
349        let original_v2 = ctx.dangerous_get_symmetric_key(v2_key_id).unwrap();
350        #[allow(deprecated)]
351        let unwrapped_v2 = ctx.dangerous_get_symmetric_key(unwrapped_v2_id).unwrap();
352        assert_eq!(original_v2, unwrapped_v2);
353    }
354}