1use std::sync::RwLock;
12
13use bitwarden_api_api::models::{
14 AccountKeysRequestModel, PrivateKeysResponseModel, SecurityStateModel,
15 WrappedAccountCryptographicStateRequestModel,
16};
17use bitwarden_crypto::{
18 CoseSerializable, CryptoError, EncString, KeyStore, KeyStoreContext,
19 PublicKeyEncryptionAlgorithm, SignatureAlgorithm, SignedPublicKey, SymmetricKeyAlgorithm,
20};
21use bitwarden_encoding::B64;
22use bitwarden_error::bitwarden_error;
23use serde::{Deserialize, Serialize};
24use thiserror::Error;
25use tracing::{info, instrument};
26#[cfg(feature = "wasm")]
27use tsify::Tsify;
28
29use crate::{
30 MissingFieldError,
31 key_management::{
32 KeySlotIds, PrivateKeySlotId, SecurityState, SignedSecurityState, SigningKeySlotId,
33 SymmetricKeySlotId,
34 },
35 require,
36};
37
38#[derive(Debug, Error)]
40#[bitwarden_error(flat)]
41pub enum AccountCryptographyInitializationError {
42 #[error("The encryption type of the user key does not match the account cryptographic state")]
45 WrongUserKeyType,
46 #[error("Wrong user key")]
49 WrongUserKey,
50 #[error("Decryption succeeded but produced corrupt data")]
52 CorruptData,
53 #[error("Signature or mac verification failed, the data may have been tampered with")]
55 TamperedData,
56 #[error("Key store is already initialized")]
59 KeyStoreAlreadyInitialized,
60 #[error("A generic cryptographic error occurred: {0}")]
62 GenericCrypto(CryptoError),
63}
64
65impl From<CryptoError> for AccountCryptographyInitializationError {
66 fn from(err: CryptoError) -> Self {
67 AccountCryptographyInitializationError::GenericCrypto(err)
68 }
69}
70
71#[derive(Debug, Error)]
73#[bitwarden_error(flat)]
74pub enum RotateCryptographyStateError {
75 #[error("The provided key is missing from the key store")]
77 KeyMissing,
78 #[error("The provided data was invalid")]
80 InvalidData,
81}
82
83#[derive(Debug, Error)]
86pub enum AccountKeysResponseParseError {
87 #[error(transparent)]
89 MissingField(#[from] MissingFieldError),
90 #[error("Malformed field value in API response")]
92 MalformedField,
93 #[error("Inconsistent account cryptographic state in API response")]
95 InconsistentState,
96}
97
98#[derive(Clone, Serialize, Deserialize, PartialEq)]
101#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
102#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
103#[allow(clippy::large_enum_variant)]
104pub enum WrappedAccountCryptographicState {
105 V1 {
107 private_key: EncString,
109 },
110 V2 {
115 private_key: EncString,
117 signed_public_key: Option<SignedPublicKey>,
121 signing_key: EncString,
123 security_state: SignedSecurityState,
125 },
126}
127
128impl std::fmt::Debug for WrappedAccountCryptographicState {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 match self {
131 WrappedAccountCryptographicState::V1 { .. } => f
132 .debug_struct("WrappedAccountCryptographicState::V1")
133 .finish(),
134 WrappedAccountCryptographicState::V2 { security_state, .. } => f
135 .debug_struct("WrappedAccountCryptographicState::V2")
136 .field("security_state", security_state)
137 .finish(),
138 }
139 }
140}
141
142impl TryFrom<&PrivateKeysResponseModel> for WrappedAccountCryptographicState {
143 type Error = AccountKeysResponseParseError;
144
145 fn try_from(response: &PrivateKeysResponseModel) -> Result<Self, Self::Error> {
146 let private_key: EncString =
147 require!(&response.public_key_encryption_key_pair.wrapped_private_key)
148 .parse()
149 .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
150
151 let is_v2_encryption = matches!(private_key, EncString::Cose_Encrypt0_B64 { .. });
152
153 if is_v2_encryption {
154 let signature_key_pair = response
155 .signature_key_pair
156 .as_ref()
157 .ok_or(AccountKeysResponseParseError::InconsistentState)?;
158
159 let signing_key: EncString = require!(&signature_key_pair.wrapped_signing_key)
160 .parse()
161 .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
162
163 let signed_public_key: Option<SignedPublicKey> = response
164 .public_key_encryption_key_pair
165 .signed_public_key
166 .as_ref()
167 .map(|spk| spk.parse())
168 .transpose()
169 .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
170
171 let security_state_model = response
172 .security_state
173 .as_ref()
174 .ok_or(AccountKeysResponseParseError::InconsistentState)?;
175 let security_state: SignedSecurityState =
176 require!(&security_state_model.security_state)
177 .parse()
178 .map_err(|_| AccountKeysResponseParseError::MalformedField)?;
179
180 Ok(WrappedAccountCryptographicState::V2 {
181 private_key,
182 signed_public_key,
183 signing_key,
184 security_state,
185 })
186 } else {
187 if response.signature_key_pair.is_some() || response.security_state.is_some() {
188 return Err(AccountKeysResponseParseError::InconsistentState);
189 }
190
191 Ok(WrappedAccountCryptographicState::V1 { private_key })
192 }
193 }
194}
195
196impl WrappedAccountCryptographicState {
197 pub fn to_wrapped_request_model(
202 &self,
203 user_key: &SymmetricKeySlotId,
204 ctx: &mut KeyStoreContext<KeySlotIds>,
205 ) -> Result<WrappedAccountCryptographicStateRequestModel, AccountCryptographyInitializationError>
206 {
207 match self {
208 WrappedAccountCryptographicState::V1 { .. } => {
209 Err(AccountCryptographyInitializationError::WrongUserKeyType)
210 }
211 WrappedAccountCryptographicState::V2 {
212 private_key,
213 signing_key,
214 security_state,
215 signed_public_key,
216 ..
217 } => {
218 let private_key = private_key.clone();
219 let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
220 let public_key = ctx.get_public_key(private_key_tmp_id)?;
221
222 let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
223 let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
224
225 Ok(WrappedAccountCryptographicStateRequestModel {
226 signature_key_pair: Box::new(
227 bitwarden_api_api::models::SignatureKeyPairRequestModel {
228 wrapped_signing_key: Some(signing_key.to_string()),
229 verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
230 signature_algorithm: Some(verifying_key.algorithm().to_string()),
231 },
232 ),
233 public_key_encryption_key_pair: Box::new(
234 bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
235 wrapped_private_key: Some(private_key.to_string()),
236 public_key: Some(B64::from(public_key.to_der()?).to_string()),
237 signed_public_key: signed_public_key.clone().map(|spk| spk.into()),
238 },
239 ),
240 security_state: Box::new(SecurityStateModel {
242 security_state: Some(security_state.into()),
243 security_version: security_state
244 .to_owned()
245 .verify_and_unwrap(&verifying_key)
246 .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
247 .version() as i32,
248 }),
249 })
250 }
251 }
252 }
253
254 #[instrument(skip_all, err)]
258 pub fn to_request_model(
259 &self,
260 user_key: &SymmetricKeySlotId,
261 ctx: &mut KeyStoreContext<KeySlotIds>,
262 ) -> Result<AccountKeysRequestModel, AccountCryptographyInitializationError> {
263 let private_key = match self {
264 WrappedAccountCryptographicState::V1 { private_key }
265 | WrappedAccountCryptographicState::V2 { private_key, .. } => private_key.clone(),
266 };
267 let private_key_tmp_id = ctx.unwrap_private_key(*user_key, &private_key)?;
268 let public_key = ctx.get_public_key(private_key_tmp_id)?;
269
270 let signature_keypair = match self {
271 WrappedAccountCryptographicState::V1 { .. } => None,
272 WrappedAccountCryptographicState::V2 { signing_key, .. } => {
273 let signing_key_tmp_id = ctx.unwrap_signing_key(*user_key, signing_key)?;
274 let verifying_key = ctx.get_verifying_key(signing_key_tmp_id)?;
275 Some((signing_key.clone(), verifying_key))
276 }
277 };
278
279 Ok(AccountKeysRequestModel {
280 user_key_encrypted_account_private_key: Some(private_key.to_string()),
282 account_public_key: Some(B64::from(public_key.to_der()?).to_string()),
284 signature_key_pair: signature_keypair
285 .as_ref()
286 .map(|(signing_key, verifying_key)| {
287 Box::new(bitwarden_api_api::models::SignatureKeyPairRequestModel {
288 wrapped_signing_key: Some(signing_key.to_string()),
289 verifying_key: Some(B64::from(verifying_key.to_cose()).to_string()),
290 signature_algorithm: Some(verifying_key.algorithm().to_string()),
291 })
292 }),
293 public_key_encryption_key_pair: Some(Box::new(
294 bitwarden_api_api::models::PublicKeyEncryptionKeyPairRequestModel {
295 wrapped_private_key: match self {
296 WrappedAccountCryptographicState::V1 { private_key }
297 | WrappedAccountCryptographicState::V2 { private_key, .. } => {
298 Some(private_key.to_string())
299 }
300 },
301 public_key: Some(B64::from(public_key.to_der()?).to_string()),
302 signed_public_key: match self.signed_public_key() {
303 Ok(Some(spk)) => Some(spk.clone().into()),
304 _ => None,
305 },
306 },
307 )),
308 security_state: match (self, signature_keypair.as_ref()) {
309 (_, None) | (WrappedAccountCryptographicState::V1 { .. }, Some(_)) => None,
310 (
311 WrappedAccountCryptographicState::V2 { security_state, .. },
312 Some((_, verifying_key)),
313 ) => {
314 Some(Box::new(SecurityStateModel {
316 security_state: Some(security_state.into()),
317 security_version: security_state
318 .to_owned()
319 .verify_and_unwrap(verifying_key)
320 .map_err(|_| AccountCryptographyInitializationError::TamperedData)?
321 .version() as i32,
322 }))
323 }
324 },
325 })
326 }
327
328 pub fn make(
331 ctx: &mut KeyStoreContext<KeySlotIds>,
332 ) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
333 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
334 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
335 let signing_key = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
336 let signed_public_key = ctx.make_signed_public_key(private_key, signing_key)?;
337
338 let security_state = SecurityState::new();
339 let signed_security_state = security_state.sign(signing_key, ctx)?;
340
341 Ok((
342 user_key,
343 WrappedAccountCryptographicState::V2 {
344 private_key: ctx.wrap_private_key(user_key, private_key)?,
345 signed_public_key: Some(signed_public_key),
346 signing_key: ctx.wrap_signing_key(user_key, signing_key)?,
347 security_state: signed_security_state,
348 },
349 ))
350 }
351
352 #[cfg(test)]
353 fn make_v1(
354 ctx: &mut KeyStoreContext<KeySlotIds>,
355 ) -> Result<(SymmetricKeySlotId, Self), AccountCryptographyInitializationError> {
356 let user_key = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
357 let private_key = ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
358
359 Ok((
360 user_key,
361 WrappedAccountCryptographicState::V1 {
362 private_key: ctx.wrap_private_key(user_key, private_key)?,
363 },
364 ))
365 }
366
367 #[instrument(skip(self, ctx), err)]
370 pub fn rotate(
371 &self,
372 current_user_key: &SymmetricKeySlotId,
373 new_user_key: &SymmetricKeySlotId,
374 ctx: &mut KeyStoreContext<KeySlotIds>,
375 ) -> Result<Self, RotateCryptographyStateError> {
376 match self {
377 WrappedAccountCryptographicState::V1 { private_key } => {
378 let private_key_id = ctx
386 .unwrap_private_key(*current_user_key, private_key)
387 .map_err(|_| RotateCryptographyStateError::InvalidData)?;
388 let new_private_key = ctx
389 .wrap_private_key(*new_user_key, private_key_id)
390 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
391
392 let signing_key_id = ctx.make_signing_key(SignatureAlgorithm::MlDsa44);
394 let new_signing_key = ctx
395 .wrap_signing_key(*new_user_key, signing_key_id)
396 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
397
398 let signed_public_key = ctx
400 .make_signed_public_key(private_key_id, signing_key_id)
401 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
402
403 let security_state = SecurityState::new();
405 let signed_security_state = security_state
406 .sign(signing_key_id, ctx)
407 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
408
409 Ok(WrappedAccountCryptographicState::V2 {
410 private_key: new_private_key,
411 signed_public_key: Some(signed_public_key),
412 signing_key: new_signing_key,
413 security_state: signed_security_state,
414 })
415 }
416 WrappedAccountCryptographicState::V2 {
417 private_key,
418 signed_public_key,
419 signing_key,
420 security_state,
421 } => {
422 let private_key_id = ctx
426 .unwrap_private_key(*current_user_key, private_key)
427 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
428 let new_private_key = ctx
429 .wrap_private_key(*new_user_key, private_key_id)
430 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
431
432 let signing_key_id = ctx
434 .unwrap_signing_key(*current_user_key, signing_key)
435 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
436 let new_signing_key = ctx
437 .wrap_signing_key(*new_user_key, signing_key_id)
438 .map_err(|_| RotateCryptographyStateError::KeyMissing)?;
439
440 Ok(WrappedAccountCryptographicState::V2 {
441 private_key: new_private_key,
442 signed_public_key: signed_public_key.clone(),
443 signing_key: new_signing_key,
444 security_state: security_state.clone(),
445 })
446 }
447 }
448 }
449
450 pub(crate) fn set_to_context(
455 &self,
456 security_state_rwlock: &RwLock<Option<SecurityState>>,
457 user_key: SymmetricKeySlotId,
458 store: &KeyStore<KeySlotIds>,
459 mut ctx: KeyStoreContext<KeySlotIds>,
460 ) -> Result<(), AccountCryptographyInitializationError> {
461 if ctx.has_symmetric_key(SymmetricKeySlotId::User)
462 || ctx.has_private_key(PrivateKeySlotId::UserPrivateKey)
463 || ctx.has_signing_key(SigningKeySlotId::UserSigningKey)
464 {
465 return Err(AccountCryptographyInitializationError::KeyStoreAlreadyInitialized);
466 }
467
468 match self {
469 WrappedAccountCryptographicState::V1 { private_key } => {
470 info!(state = ?self, "Initializing V1 account cryptographic state");
471 if ctx.get_symmetric_key_algorithm(user_key)?
472 != SymmetricKeyAlgorithm::Aes256CbcHmac
473 {
474 return Err(AccountCryptographyInitializationError::WrongUserKeyType);
475 }
476
477 if let Ok(private_key_id) = ctx.unwrap_private_key(user_key, private_key) {
480 ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
481 } else {
482 tracing::warn!(
483 "V1 private key could not be unwrapped, skipping setting private key"
484 );
485 }
486
487 ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
488 #[cfg(feature = "dangerous-crypto-debug")]
489 #[allow(deprecated)]
490 {
491 let user_key = ctx
492 .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
493 .expect("User key should be set");
494 let private_key = ctx
495 .dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
496 .ok();
497 let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
498 info!(
499 ?user_key,
500 ?private_key,
501 ?public_key,
502 "V1 account cryptographic state set to context"
503 );
504 }
505 }
506 WrappedAccountCryptographicState::V2 {
507 private_key,
508 signed_public_key,
509 signing_key,
510 security_state,
511 } => {
512 info!(state = ?self, "Initializing V2 account cryptographic state");
513 if ctx.get_symmetric_key_algorithm(user_key)?
514 != SymmetricKeyAlgorithm::XChaCha20Poly1305
515 {
516 return Err(AccountCryptographyInitializationError::WrongUserKeyType);
517 }
518
519 let private_key_id = ctx
520 .unwrap_private_key(user_key, private_key)
521 .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
522 let signing_key_id = ctx
523 .unwrap_signing_key(user_key, signing_key)
524 .map_err(|_| AccountCryptographyInitializationError::WrongUserKey)?;
525
526 if let Some(signed_public_key) = signed_public_key {
527 signed_public_key
528 .to_owned()
529 .verify_and_unwrap(&ctx.get_verifying_key(signing_key_id)?)
530 .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
531 }
532
533 let verifying_key = ctx.get_verifying_key(signing_key_id)?;
534 let security_state: SecurityState = security_state
535 .to_owned()
536 .verify_and_unwrap(&verifying_key)
537 .map_err(|_| AccountCryptographyInitializationError::TamperedData)?;
538 info!(
539 security_state_version = security_state.version(),
540 verifying_key = ?verifying_key,
541 "V2 account cryptographic state verified"
542 );
543 ctx.persist_private_key(private_key_id, PrivateKeySlotId::UserPrivateKey)?;
544 ctx.persist_signing_key(signing_key_id, SigningKeySlotId::UserSigningKey)?;
545 ctx.persist_symmetric_key(user_key, SymmetricKeySlotId::User)?;
546
547 #[cfg(feature = "dangerous-crypto-debug")]
548 #[allow(deprecated)]
549 {
550 let user_key = ctx
551 .dangerous_get_symmetric_key(SymmetricKeySlotId::User)
552 .expect("User key should be set");
553 let private_key = ctx
554 .dangerous_get_private_key(PrivateKeySlotId::UserPrivateKey)
555 .ok();
556 let signing_key = ctx
557 .dangerous_get_signing_key(SigningKeySlotId::UserSigningKey)
558 .ok();
559 let verifying_key =
560 ctx.get_verifying_key(SigningKeySlotId::UserSigningKey).ok();
561 let public_key = ctx.get_public_key(PrivateKeySlotId::UserPrivateKey).ok();
562 info!(
563 ?user_key,
564 ?private_key,
565 ?signing_key,
566 ?verifying_key,
567 ?public_key,
568 ?signed_public_key,
569 ?security_state,
570 "V2 account cryptographic state set to context."
571 );
572 }
573
574 drop(ctx);
577 store.set_security_state_version(security_state.version());
578 *security_state_rwlock.write().expect("RwLock not poisoned") = Some(security_state);
579 }
580 }
581
582 Ok(())
583 }
584
585 fn signed_public_key(
587 &self,
588 ) -> Result<Option<&SignedPublicKey>, AccountCryptographyInitializationError> {
589 match self {
590 WrappedAccountCryptographicState::V1 { .. } => Ok(None),
591 WrappedAccountCryptographicState::V2 {
592 signed_public_key, ..
593 } => Ok(signed_public_key.as_ref()),
594 }
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use std::{str::FromStr, sync::RwLock};
601
602 use bitwarden_crypto::{KeyStore, PrimitiveEncryptable};
603
604 use super::*;
605 use crate::key_management::{PrivateKeySlotId, SigningKeySlotId, SymmetricKeySlotId};
606
607 #[test]
608 #[ignore = "Manual test to verify debug format"]
609 fn test_debug() {
610 let store: KeyStore<KeySlotIds> = KeyStore::default();
611 let mut ctx = store.context_mut();
612
613 let (_, v1) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
614 println!("{:?}", v1);
615
616 let v1 = format!("{v1:?}");
617 assert!(!v1.contains("private_key"));
618
619 let (_, v2) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
620 println!("{:?}", v2);
621
622 let v2 = format!("{v2:?}");
623 assert!(!v2.contains("private_key"));
624 assert!(!v2.contains("signed_public_key"));
625 assert!(!v2.contains("signing_key"));
626 }
627
628 #[test]
629 fn test_set_to_context_v1() {
630 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
632 let mut temp_ctx = temp_store.context_mut();
633
634 let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
636
637 let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
639 let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
640
641 let wrapped = WrappedAccountCryptographicState::V1 {
643 private_key: wrapped_private,
644 };
645 #[allow(deprecated)]
646 let user_key = temp_ctx
647 .dangerous_get_symmetric_key(user_key)
648 .unwrap()
649 .to_owned();
650 drop(temp_ctx);
651 drop(temp_store);
652
653 let store: KeyStore<KeySlotIds> = KeyStore::default();
655 let mut ctx = store.context_mut();
656 let user_key = ctx.add_local_symmetric_key(user_key);
657 let security_state = RwLock::new(None);
658
659 wrapped
661 .set_to_context(&security_state, user_key, &store, ctx)
662 .unwrap();
663 let ctx = store.context();
664
665 assert!(ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
667 assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
668 }
669
670 #[test]
671 fn test_set_to_context_v2() {
672 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
674 let mut temp_ctx = temp_store.context_mut();
675
676 let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
678
679 let private_key_id = temp_ctx.make_private_key(PublicKeyEncryptionAlgorithm::RsaOaepSha1);
681 let signing_key_id = temp_ctx.make_signing_key(SignatureAlgorithm::Ed25519);
682 let signed_public_key = temp_ctx
683 .make_signed_public_key(private_key_id, signing_key_id)
684 .unwrap();
685
686 let security_state = SecurityState::new();
688 let signed_security_state = security_state.sign(signing_key_id, &mut temp_ctx).unwrap();
689
690 let wrapped_private = temp_ctx.wrap_private_key(user_key, private_key_id).unwrap();
692 let wrapped_signing = temp_ctx.wrap_signing_key(user_key, signing_key_id).unwrap();
693
694 let wrapped = WrappedAccountCryptographicState::V2 {
695 private_key: wrapped_private,
696 signed_public_key: Some(signed_public_key),
697 signing_key: wrapped_signing,
698 security_state: signed_security_state,
699 };
700 #[allow(deprecated)]
701 let user_key = temp_ctx
702 .dangerous_get_symmetric_key(user_key)
703 .unwrap()
704 .to_owned();
705 drop(temp_ctx);
706 drop(temp_store);
707
708 let store: KeyStore<KeySlotIds> = KeyStore::default();
710 let mut ctx = store.context_mut();
711 let user_key = ctx.add_local_symmetric_key(user_key);
712 let security_state = RwLock::new(None);
713
714 wrapped
715 .set_to_context(&security_state, user_key, &store, ctx)
716 .unwrap();
717
718 assert!(store.context().has_symmetric_key(SymmetricKeySlotId::User));
719 assert!(
721 store
722 .context()
723 .has_private_key(PrivateKeySlotId::UserPrivateKey)
724 );
725 assert!(
726 store
727 .context()
728 .has_signing_key(SigningKeySlotId::UserSigningKey)
729 );
730 assert!(security_state.read().unwrap().is_some());
732 }
733
734 #[test]
735 fn test_to_private_keys_request_model_v2() {
736 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
737 let mut temp_ctx = temp_store.context_mut();
738 let (user_key, wrapped_account_cryptography_state) =
739 WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
740
741 wrapped_account_cryptography_state
742 .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
743 .unwrap();
744
745 let mut ctx = temp_store.context_mut();
746 let model = wrapped_account_cryptography_state
747 .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
748 .expect("to_private_keys_request_model should succeed");
749 drop(ctx);
750
751 let ctx = temp_store.context();
752
753 let sig_pair = model
754 .signature_key_pair
755 .expect("signature_key_pair present");
756 assert_eq!(
757 sig_pair.verifying_key.unwrap(),
758 B64::from(
759 ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
760 .unwrap()
761 .to_cose()
762 )
763 .to_string()
764 );
765
766 let pk_pair = model.public_key_encryption_key_pair.unwrap();
767 assert_eq!(
768 pk_pair.public_key.unwrap(),
769 B64::from(
770 ctx.get_public_key(PrivateKeySlotId::UserPrivateKey)
771 .unwrap()
772 .to_der()
773 .unwrap()
774 )
775 .to_string()
776 );
777
778 let signed_security_state = model
779 .security_state
780 .clone()
781 .expect("security_state present");
782 let security_state =
783 SignedSecurityState::from_str(signed_security_state.security_state.unwrap().as_str())
784 .unwrap()
785 .verify_and_unwrap(
786 &ctx.get_verifying_key(SigningKeySlotId::UserSigningKey)
787 .unwrap(),
788 )
789 .expect("security state should verify");
790 assert_eq!(
791 security_state.version(),
792 model.security_state.unwrap().security_version as u64
793 );
794 }
795
796 #[test]
797 fn test_set_to_context_v1_corrupt_private_key() {
798 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
801 let mut temp_ctx = temp_store.context_mut();
802
803 let user_key = temp_ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
804 let corrupt_private_key = "not a private key"
805 .encrypt(&mut temp_ctx, user_key)
806 .unwrap();
807
808 let wrapped = WrappedAccountCryptographicState::V1 {
810 private_key: corrupt_private_key,
811 };
812
813 #[expect(deprecated)]
814 let user_key_material = temp_ctx
815 .dangerous_get_symmetric_key(user_key)
816 .unwrap()
817 .to_owned();
818 drop(temp_ctx);
819 drop(temp_store);
820
821 let store: KeyStore<KeySlotIds> = KeyStore::default();
823 let mut ctx = store.context_mut();
824 let user_key = ctx.add_local_symmetric_key(user_key_material);
825 let security_state = RwLock::new(None);
826
827 wrapped
828 .set_to_context(&security_state, user_key, &store, ctx)
829 .unwrap();
830
831 let ctx = store.context();
832
833 assert!(ctx.has_symmetric_key(SymmetricKeySlotId::User));
835 assert!(!ctx.has_private_key(PrivateKeySlotId::UserPrivateKey));
837 }
838
839 #[test]
840 fn test_try_from_response_v2_roundtrip() {
841 use bitwarden_api_api::models::{
842 PublicKeyEncryptionKeyPairResponseModel, SecurityStateModel,
843 SignatureKeyPairResponseModel,
844 };
845
846 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
847 let mut temp_ctx = temp_store.context_mut();
848 let (user_key, wrapped_state) =
849 WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
850
851 wrapped_state
852 .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
853 .unwrap();
854
855 let mut ctx = temp_store.context_mut();
856 let request_model = wrapped_state
857 .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
858 .unwrap();
859 drop(ctx);
860
861 let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
862 let sig_pair = request_model.signature_key_pair.unwrap();
863 let sec_state = request_model.security_state.unwrap();
864
865 let response = PrivateKeysResponseModel {
866 object: None,
867 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
868 object: None,
869 wrapped_private_key: pk_pair.wrapped_private_key,
870 public_key: pk_pair.public_key,
871 signed_public_key: pk_pair.signed_public_key,
872 }),
873 signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
874 object: None,
875 wrapped_signing_key: sig_pair.wrapped_signing_key,
876 verifying_key: sig_pair.verifying_key,
877 })),
878 security_state: Some(Box::new(SecurityStateModel {
879 security_state: sec_state.security_state,
880 security_version: sec_state.security_version,
881 })),
882 };
883
884 let parsed = WrappedAccountCryptographicState::try_from(&response)
885 .expect("V2 response should parse successfully");
886
887 assert_eq!(parsed, wrapped_state);
888 }
889
890 #[test]
891 fn test_try_from_response_v1() {
892 use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
893
894 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
895 let mut temp_ctx = temp_store.context_mut();
896 let (_user_key, wrapped_state) =
897 WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
898
899 let wrapped_private_key = match &wrapped_state {
900 WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
901 _ => panic!("Expected V1"),
902 };
903 drop(temp_ctx);
904
905 let response = PrivateKeysResponseModel {
906 object: None,
907 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
908 object: None,
909 wrapped_private_key: Some(wrapped_private_key),
910 public_key: None,
911 signed_public_key: None,
912 }),
913 signature_key_pair: None,
914 security_state: None,
915 };
916
917 let parsed = WrappedAccountCryptographicState::try_from(&response)
918 .expect("V1 response should parse successfully");
919
920 assert_eq!(parsed, wrapped_state);
921 }
922
923 #[test]
924 fn test_try_from_response_missing_private_key() {
925 use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
926
927 let response = PrivateKeysResponseModel {
928 object: None,
929 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
930 object: None,
931 wrapped_private_key: None,
932 public_key: None,
933 signed_public_key: None,
934 }),
935 signature_key_pair: None,
936 security_state: None,
937 };
938
939 let result = WrappedAccountCryptographicState::try_from(&response);
940 assert!(result.is_err());
941 assert!(
942 matches!(
943 result.unwrap_err(),
944 AccountKeysResponseParseError::MissingField(_)
945 ),
946 "Should return MissingField error"
947 );
948 }
949
950 #[test]
951 fn test_try_from_response_v2_encryption_missing_signature_key_pair() {
952 use bitwarden_api_api::models::PublicKeyEncryptionKeyPairResponseModel;
953
954 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
956 let mut temp_ctx = temp_store.context_mut();
957 let (user_key, wrapped_state) =
958 WrappedAccountCryptographicState::make(&mut temp_ctx).unwrap();
959
960 wrapped_state
961 .set_to_context(&RwLock::new(None), user_key, &temp_store, temp_ctx)
962 .unwrap();
963
964 let mut ctx = temp_store.context_mut();
965 let request_model = wrapped_state
966 .to_request_model(&SymmetricKeySlotId::User, &mut ctx)
967 .unwrap();
968 drop(ctx);
969
970 let pk_pair = request_model.public_key_encryption_key_pair.unwrap();
971
972 let response = PrivateKeysResponseModel {
974 object: None,
975 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
976 object: None,
977 wrapped_private_key: pk_pair.wrapped_private_key,
978 public_key: pk_pair.public_key,
979 signed_public_key: None,
980 }),
981 signature_key_pair: None,
982 security_state: None,
983 };
984
985 let result = WrappedAccountCryptographicState::try_from(&response);
986 assert!(matches!(
987 result.unwrap_err(),
988 AccountKeysResponseParseError::InconsistentState
989 ));
990 }
991
992 #[test]
993 fn test_try_from_response_v1_encryption_with_unexpected_v2_fields() {
994 use bitwarden_api_api::models::{
995 PublicKeyEncryptionKeyPairResponseModel, SignatureKeyPairResponseModel,
996 };
997
998 let temp_store: KeyStore<KeySlotIds> = KeyStore::default();
1000 let mut temp_ctx = temp_store.context_mut();
1001 let (_user_key, wrapped_state) =
1002 WrappedAccountCryptographicState::make_v1(&mut temp_ctx).unwrap();
1003
1004 let wrapped_private_key = match &wrapped_state {
1005 WrappedAccountCryptographicState::V1 { private_key } => private_key.to_string(),
1006 _ => panic!("Expected V1"),
1007 };
1008 drop(temp_ctx);
1009
1010 let response = PrivateKeysResponseModel {
1012 object: None,
1013 public_key_encryption_key_pair: Box::new(PublicKeyEncryptionKeyPairResponseModel {
1014 object: None,
1015 wrapped_private_key: Some(wrapped_private_key),
1016 public_key: None,
1017 signed_public_key: None,
1018 }),
1019 signature_key_pair: Some(Box::new(SignatureKeyPairResponseModel {
1020 object: None,
1021 wrapped_signing_key: Some("bogus".to_string()),
1022 verifying_key: None,
1023 })),
1024 security_state: None,
1025 };
1026
1027 let result = WrappedAccountCryptographicState::try_from(&response);
1028 assert!(matches!(
1029 result.unwrap_err(),
1030 AccountKeysResponseParseError::InconsistentState
1031 ));
1032 }
1033
1034 #[test]
1035 fn test_rotate_v1_to_v2() {
1036 let store: KeyStore<KeySlotIds> = KeyStore::default();
1038 let mut ctx = store.context_mut();
1039
1040 let (old_user_key_id, wrapped_state) =
1042 WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1043 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1044 #[allow(deprecated)]
1045 let new_user_key_owned = ctx
1046 .dangerous_get_symmetric_key(new_user_key_id)
1047 .unwrap()
1048 .to_owned();
1049 wrapped_state
1050 .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1051 .unwrap();
1052
1053 let mut ctx = store.context_mut();
1056 let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1057
1058 let rotated_state = wrapped_state
1060 .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1061 .unwrap();
1062
1063 match rotated_state {
1069 WrappedAccountCryptographicState::V2 { .. } => {}
1070 _ => panic!("Expected V2 after rotation from V1"),
1071 }
1072 let store_2 = KeyStore::<KeySlotIds>::default();
1073 let mut ctx_2 = store_2.context_mut();
1074 let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1075 rotated_state
1076 .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1077 .unwrap();
1078 let ctx_2 = store_2.context();
1080
1081 let public_key_before_rotation = ctx
1083 .get_public_key(PrivateKeySlotId::UserPrivateKey)
1084 .expect("Private key should be present in context before rotation");
1085 let public_key_after_rotation = ctx_2
1086 .get_public_key(PrivateKeySlotId::UserPrivateKey)
1087 .expect("Private key should be present in context after rotation");
1088 assert_eq!(
1089 public_key_before_rotation.to_der().unwrap(),
1090 public_key_after_rotation.to_der().unwrap(),
1091 "Private key should be preserved during rotation from V2 to V2"
1092 );
1093 }
1094
1095 #[test]
1096 fn test_rotate_v2() {
1097 let store: KeyStore<KeySlotIds> = KeyStore::default();
1099 let mut ctx = store.context_mut();
1100
1101 let (old_user_key_id, wrapped_state) =
1103 WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1104 let new_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1105 #[allow(deprecated)]
1106 let new_user_key_owned = ctx
1107 .dangerous_get_symmetric_key(new_user_key_id)
1108 .unwrap()
1109 .to_owned();
1110 wrapped_state
1111 .set_to_context(&RwLock::new(None), old_user_key_id, &store, ctx)
1112 .unwrap();
1113
1114 let mut ctx = store.context_mut();
1117 let new_user_key_id = ctx.add_local_symmetric_key(new_user_key_owned.clone());
1118
1119 let rotated_state = wrapped_state
1121 .rotate(&SymmetricKeySlotId::User, &new_user_key_id, &mut ctx)
1122 .unwrap();
1123
1124 match rotated_state {
1130 WrappedAccountCryptographicState::V2 { .. } => {}
1131 _ => panic!("Expected V2 after rotation from V2"),
1132 }
1133 let store_2 = KeyStore::<KeySlotIds>::default();
1134 let mut ctx_2 = store_2.context_mut();
1135 let user_key_id = ctx_2.add_local_symmetric_key(new_user_key_owned.clone());
1136 rotated_state
1137 .set_to_context(&RwLock::new(None), user_key_id, &store_2, ctx_2)
1138 .unwrap();
1139 let ctx_2 = store_2.context();
1141
1142 let verifying_key_before_rotation = ctx
1144 .get_verifying_key(SigningKeySlotId::UserSigningKey)
1145 .expect("Signing key should be present in context before rotation");
1146 let verifying_key_after_rotation = ctx_2
1147 .get_verifying_key(SigningKeySlotId::UserSigningKey)
1148 .expect("Signing key should be present in context after rotation");
1149 assert_eq!(
1150 verifying_key_before_rotation.to_cose(),
1151 verifying_key_after_rotation.to_cose(),
1152 "Signing key should be preserved during rotation from V2 to V2"
1153 );
1154
1155 let public_key_before_rotation = ctx
1156 .get_public_key(PrivateKeySlotId::UserPrivateKey)
1157 .expect("Private key should be present in context before rotation");
1158 let public_key_after_rotation = ctx_2
1159 .get_public_key(PrivateKeySlotId::UserPrivateKey)
1160 .expect("Private key should be present in context after rotation");
1161 assert_eq!(
1162 public_key_before_rotation.to_der().unwrap(),
1163 public_key_after_rotation.to_der().unwrap(),
1164 "Private key should be preserved during rotation from V2 to V2"
1165 );
1166 }
1167
1168 #[test]
1169 fn test_to_wrapped_request_model_v1_returns_wrong_user_key_type() {
1170 let store: KeyStore<KeySlotIds> = KeyStore::default();
1171 let mut ctx = store.context_mut();
1172 let (user_key_id, wrapped) = WrappedAccountCryptographicState::make_v1(&mut ctx).unwrap();
1173 let result = wrapped.to_wrapped_request_model(&user_key_id, &mut ctx);
1174 assert!(matches!(
1175 result.unwrap_err(),
1176 AccountCryptographyInitializationError::WrongUserKeyType
1177 ));
1178 }
1179
1180 #[test]
1181 fn test_to_wrapped_request_model_v2() {
1182 let store: KeyStore<KeySlotIds> = KeyStore::default();
1183 let mut ctx = store.context_mut();
1184 let (user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1185 let result = wrapped
1186 .to_wrapped_request_model(&user_key_id, &mut ctx)
1187 .unwrap();
1188
1189 let wrapped_signing_key_str = result
1190 .signature_key_pair
1191 .wrapped_signing_key
1192 .as_ref()
1193 .unwrap();
1194 assert!(!wrapped_signing_key_str.is_empty());
1195
1196 let enc_signing_key: EncString = wrapped_signing_key_str.parse().unwrap();
1197 let signing_key_tmp = ctx
1198 .unwrap_signing_key(user_key_id, &enc_signing_key)
1199 .unwrap();
1200 let verifying_key = ctx.get_verifying_key(signing_key_tmp).unwrap();
1201 let expected = B64::from(verifying_key.to_cose()).to_string();
1202 assert!(
1203 result
1204 .signature_key_pair
1205 .verifying_key
1206 .as_ref()
1207 .is_some_and(|s| s == &expected),
1208 "verifying_key should match expected value"
1209 );
1210
1211 assert_eq!(
1212 result.signature_key_pair.signature_algorithm.as_deref(),
1213 Some("mldsa44")
1214 );
1215
1216 assert!(
1217 result
1218 .public_key_encryption_key_pair
1219 .wrapped_private_key
1220 .as_ref()
1221 .is_some_and(|s| !s.is_empty()),
1222 "wrapped_private_key should be non-empty"
1223 );
1224 let wrapped_private_key_str = result
1225 .public_key_encryption_key_pair
1226 .wrapped_private_key
1227 .as_ref()
1228 .unwrap();
1229 let enc_private_key: EncString = wrapped_private_key_str.parse().unwrap();
1230 let private_key_tmp = ctx
1231 .unwrap_private_key(user_key_id, &enc_private_key)
1232 .unwrap();
1233 let public_key = ctx.get_public_key(private_key_tmp).unwrap();
1234
1235 let expected = B64::from(public_key.to_der().unwrap()).to_string();
1236 assert!(
1237 result
1238 .public_key_encryption_key_pair
1239 .public_key
1240 .as_ref()
1241 .is_some_and(|s| s == &expected),
1242 "public_key should match expected value"
1243 );
1244 assert!(
1245 result
1246 .public_key_encryption_key_pair
1247 .signed_public_key
1248 .is_some(),
1249 "signed_public_key should be present"
1250 );
1251 assert!(
1252 result
1253 .security_state
1254 .security_state
1255 .as_ref()
1256 .is_some_and(|s| !s.is_empty()),
1257 "security_state string should be non-empty"
1258 );
1259 assert!(result.security_state.security_version == 2);
1260 }
1261
1262 #[test]
1263 fn test_to_wrapped_request_model_wrong_user_key_returns_error() {
1264 let store: KeyStore<KeySlotIds> = KeyStore::default();
1265 let mut ctx = store.context_mut();
1266 let (_user_key_id, wrapped) = WrappedAccountCryptographicState::make(&mut ctx).unwrap();
1267
1268 let wrong_user_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::XChaCha20Poly1305);
1270
1271 let result = wrapped.to_wrapped_request_model(&wrong_user_key_id, &mut ctx);
1272 assert!(result.is_err());
1273 assert!(!matches!(
1275 result.unwrap_err(),
1276 AccountCryptographyInitializationError::WrongUserKeyType
1277 ));
1278 }
1279}