|
1 | 1 | use std::collections::HashMap;
|
2 | 2 |
|
| 3 | +use base64::{engine::general_purpose::STANDARD, Engine}; |
3 | 4 | use bitwarden_crypto::{
|
4 |
| - AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey, |
5 |
| - SymmetricCryptoKey, |
| 5 | + AsymmetricCryptoKey, AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable, |
| 6 | + MasterKey, SymmetricCryptoKey, UserKey, |
6 | 7 | };
|
7 | 8 | use schemars::JsonSchema;
|
8 | 9 | use serde::{Deserialize, Serialize};
|
@@ -350,10 +351,117 @@ pub(super) fn derive_key_connector(request: DeriveKeyConnectorRequest) -> Result
|
350 | 351 | Ok(master_key.to_base64())
|
351 | 352 | }
|
352 | 353 |
|
| 354 | +#[derive(Serialize, Deserialize, Debug, JsonSchema)] |
| 355 | +#[serde(rename_all = "camelCase", deny_unknown_fields)] |
| 356 | +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] |
| 357 | +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] |
| 358 | +pub struct MakeKeyPairResponse { |
| 359 | + /// The user's public key |
| 360 | + user_public_key: String, |
| 361 | + /// User's private key, encrypted with the user key |
| 362 | + user_key_encrypted_private_key: EncString, |
| 363 | +} |
| 364 | + |
| 365 | +pub fn make_key_pair(client: &Client) -> Result<MakeKeyPairResponse> { |
| 366 | + let enc = client.internal.get_encryption_settings()?; |
| 367 | + let user_key = UserKey::new(enc.get_key(&None)?.clone()); |
| 368 | + |
| 369 | + let key_pair = user_key.make_key_pair()?; |
| 370 | + |
| 371 | + Ok(MakeKeyPairResponse { |
| 372 | + user_public_key: key_pair.public, |
| 373 | + user_key_encrypted_private_key: key_pair.private, |
| 374 | + }) |
| 375 | +} |
| 376 | + |
| 377 | +#[derive(Serialize, Deserialize, Debug, JsonSchema)] |
| 378 | +#[serde(rename_all = "camelCase", deny_unknown_fields)] |
| 379 | +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] |
| 380 | +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] |
| 381 | +pub struct VerifyAsymmetricKeysRequest { |
| 382 | + /// The user's public key |
| 383 | + user_public_key: String, |
| 384 | + |
| 385 | + /// User's private key, encrypted with the user key |
| 386 | + user_key_encrypted_private_key: EncString, |
| 387 | +} |
| 388 | + |
| 389 | +#[derive(Serialize, Deserialize, Debug, JsonSchema)] |
| 390 | +#[serde(rename_all = "camelCase", deny_unknown_fields)] |
| 391 | +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] |
| 392 | +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] |
| 393 | +pub struct VerifyAsymmetricKeysResponse { |
| 394 | + /// Whether the user's private key was decryptable by the user key. |
| 395 | + private_key_decryptable: bool, |
| 396 | + /// Whether the user's private key was a valid RSA key and matched the public key provided. |
| 397 | + valid_private_key: bool, |
| 398 | +} |
| 399 | + |
| 400 | +pub fn verify_asymmetric_keys( |
| 401 | + client: &Client, |
| 402 | + request: VerifyAsymmetricKeysRequest, |
| 403 | +) -> Result<VerifyAsymmetricKeysResponse> { |
| 404 | + #[derive(Debug, thiserror::Error)] |
| 405 | + enum VerifyError { |
| 406 | + #[error("Failed to decrypt private key: {0:?}")] |
| 407 | + DecryptFailed(bitwarden_crypto::CryptoError), |
| 408 | + #[error("Failed to parse decrypted private key: {0:?}")] |
| 409 | + ParseFailed(bitwarden_crypto::CryptoError), |
| 410 | + #[error("Failed to derive a public key: {0:?}")] |
| 411 | + PublicFailed(bitwarden_crypto::CryptoError), |
| 412 | + #[error("Derived public key doesn't match")] |
| 413 | + KeyMismatch, |
| 414 | + } |
| 415 | + |
| 416 | + fn verify_inner( |
| 417 | + user_key: &SymmetricCryptoKey, |
| 418 | + request: &VerifyAsymmetricKeysRequest, |
| 419 | + ) -> Result<(), VerifyError> { |
| 420 | + let decrypted_private_key: Vec<u8> = request |
| 421 | + .user_key_encrypted_private_key |
| 422 | + .decrypt_with_key(user_key) |
| 423 | + .map_err(VerifyError::DecryptFailed)?; |
| 424 | + |
| 425 | + let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key) |
| 426 | + .map_err(VerifyError::ParseFailed)?; |
| 427 | + |
| 428 | + let derived_public_key_vec = private_key |
| 429 | + .to_public_der() |
| 430 | + .map_err(VerifyError::PublicFailed)?; |
| 431 | + |
| 432 | + let derived_public_key = STANDARD.encode(&derived_public_key_vec); |
| 433 | + |
| 434 | + if derived_public_key != request.user_public_key { |
| 435 | + return Err(VerifyError::KeyMismatch); |
| 436 | + } |
| 437 | + Ok(()) |
| 438 | + } |
| 439 | + |
| 440 | + let enc = client.internal.get_encryption_settings()?; |
| 441 | + let user_key = enc.get_key(&None)?; |
| 442 | + |
| 443 | + Ok(match verify_inner(user_key, &request) { |
| 444 | + Ok(_) => VerifyAsymmetricKeysResponse { |
| 445 | + private_key_decryptable: true, |
| 446 | + valid_private_key: true, |
| 447 | + }, |
| 448 | + Err(e) => { |
| 449 | + log::debug!("User asymmetric keys verification: {}", e); |
| 450 | + |
| 451 | + VerifyAsymmetricKeysResponse { |
| 452 | + private_key_decryptable: !matches!(e, VerifyError::DecryptFailed(_)), |
| 453 | + valid_private_key: false, |
| 454 | + } |
| 455 | + } |
| 456 | + }) |
| 457 | +} |
| 458 | + |
353 | 459 | #[cfg(test)]
|
354 | 460 | mod tests {
|
355 | 461 | use std::num::NonZeroU32;
|
356 | 462 |
|
| 463 | + use bitwarden_crypto::RsaKeyPair; |
| 464 | + |
357 | 465 | use super::*;
|
358 | 466 | use crate::Client;
|
359 | 467 |
|
@@ -585,4 +693,105 @@ mod tests {
|
585 | 693 |
|
586 | 694 | assert_eq!(result, "ySXq1RVLKEaV1eoQE/ui9aFKIvXTl9PAXwp1MljfF50=");
|
587 | 695 | }
|
| 696 | + |
| 697 | + fn setup_asymmetric_keys_test() -> (Client, UserKey, RsaKeyPair) { |
| 698 | + let client = Client::new(None); |
| 699 | + |
| 700 | + let master_key = MasterKey::derive( |
| 701 | + "asdfasdfasdf", |
| 702 | + |
| 703 | + &Kdf::PBKDF2 { |
| 704 | + iterations: NonZeroU32::new(600_000).unwrap(), |
| 705 | + }, |
| 706 | + ) |
| 707 | + .unwrap(); |
| 708 | + let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap(); |
| 709 | + let key_pair = user_key.make_key_pair().unwrap(); |
| 710 | + |
| 711 | + client |
| 712 | + .internal |
| 713 | + .initialize_user_crypto_master_key( |
| 714 | + master_key, |
| 715 | + encrypted_user_key, |
| 716 | + key_pair.private.clone(), |
| 717 | + ) |
| 718 | + .unwrap(); |
| 719 | + (client, user_key, key_pair) |
| 720 | + } |
| 721 | + |
| 722 | + #[test] |
| 723 | + fn test_make_key_pair() { |
| 724 | + let (client, user_key, _) = setup_asymmetric_keys_test(); |
| 725 | + |
| 726 | + let response = make_key_pair(&client).unwrap(); |
| 727 | + |
| 728 | + assert!(!response.user_public_key.is_empty()); |
| 729 | + let encrypted_private_key = response.user_key_encrypted_private_key; |
| 730 | + let private_key: Vec<u8> = encrypted_private_key.decrypt_with_key(&user_key.0).unwrap(); |
| 731 | + assert!(!private_key.is_empty()); |
| 732 | + } |
| 733 | + |
| 734 | + #[test] |
| 735 | + fn test_verify_asymmetric_keys_success() { |
| 736 | + let (client, _, key_pair) = setup_asymmetric_keys_test(); |
| 737 | + |
| 738 | + let request = VerifyAsymmetricKeysRequest { |
| 739 | + user_public_key: key_pair.public, |
| 740 | + user_key_encrypted_private_key: key_pair.private, |
| 741 | + }; |
| 742 | + let response = client.crypto().verify_asymmetric_keys(request).unwrap(); |
| 743 | + |
| 744 | + assert!(response.private_key_decryptable); |
| 745 | + assert!(response.valid_private_key); |
| 746 | + } |
| 747 | + |
| 748 | + #[test] |
| 749 | + fn test_verify_asymmetric_keys_decrypt_failed() { |
| 750 | + let (client, _, key_pair) = setup_asymmetric_keys_test(); |
| 751 | + let undecryptable_private_key = "2.cqD39M4erPZ3tWaz2Fng9w==|+Bsp/xvM30oo+HThKN12qirK0A63EjMadcwethCX7kEgfL5nEXgAFsSgRBMpByc1djgpGDMXzUTLOE+FejXRsrEHH/ICZ7jPMgSR+lV64Mlvw3fgvDPQdJ6w3MCmjPueGQtrlPj1K78BkRomN3vQwwRBFUIJhLAnLshTOIFrSghoyG78na7McqVMMD0gmC0zmRaSs2YWu/46ES+2Rp8V5OC4qdeeoJM9MQfaOtmaqv7NRVDeDM3DwoyTJAOcon8eovMKE4jbFPUboiXjNQBkBgjvLhco3lVJnFcQuYgmjqrwuUQRsfAtZjxFXg/RQSH2D+SI5uRaTNQwkL4iJqIw7BIKtI0gxDz6eCVdq/+DLhpImgCV/aaIhF/jkpGqLCceFsYMbuqdULMM1VYKgV+IAuyC65R+wxOaKS+1IevvPnNp7tgKAvT5+shFg8piusj+rQ49daX2SmV2OImwdWMmmX93bcVV0xJ/WYB1yrqmyRUcTwyvX3RQF25P5okIIzFasRp8jXFZe8C6f93yzkn1TPQbp95zF4OsWjfPFVH4hzca07ACt2HjbAB75JakWbFA5MbCF8aOIwIfeLVhVlquQXCldOHCsl22U/f3HTGLB9OS8F83CDAy7qZqpKha9Im8RUhHoyf+lXrky0gyd6un7Ky8NSkVOGd8CEG7bvZfutxv/qtAjEM9/lV78fh8TQIy9GNgioMzplpuzPIJOgMaY/ZFZj6a8H9OMPneN5Je0H/DwHEglSyWy7CMgwcbQgXYGXc8rXTTxL71GUAFHzDr4bAJvf40YnjndoL9tf+oBw8vVNUccoD4cjyOT5w8h7M3Liaxk9/0O8JR98PKxxpv1Xw6XjFCSEHeG2y9FgDUASFR4ZwG1qQBiiLMnJ7e9kvxsdnmasBux9H0tOdhDhAM16Afk3NPPKA8eztJVHJBAfQiaNiUA4LIJ48d8EpUAe2Tvz0WW/gQThplUINDTpvPf+FojLwc5lFwNIPb4CVN1Ui8jOJI5nsOw4BSWJvLzJLxawHxX/sBuK96iXza+4aMH+FqYKt/twpTJtiVXo26sPtHe6xXtp7uO4b+bL9yYUcaAci69L0W8aNdu8iF0lVX6kFn2lOL8dBLRleGvixX9gYEVEsiI7BQBjxEBHW/YMr5F4M4smqCpleZIAxkse1r2fQ33BSOJVQKInt4zzgdKwrxDzuVR7RyiIUuNXHsprKtRHNJrSc4x5kWFUeivahed2hON+Ir/ZvrxYN6nJJPeYYH4uEm1Nn4osUzzfWILlqpmDPK1yYy365T38W8wT0cbdcJrI87ycS37HeB8bzpFJZSY/Dzv48Yy19mDZJHLJLCRqyxNeIlBPsVC8fvxQhzr+ZyS3Wi8Dsa2Sgjt/wd0xPULLCJlb37s+1aWgYYylr9QR1uhXheYfkXFED+saGWwY1jlYL5e2Oo9n3sviBYwJxIZ+RTKFgwlXV5S+Jx/MbDpgnVHP1KaoU6vvzdWYwMChdHV/6PhZVbeT2txq7Qt+zQN59IGrOWf6vlMkHxfUzMTD58CE+xAaz/D05ljHMesLj9hb3MSrymw0PcwoFGWUMIzIQE73pUVYNE7fVHa8HqUOdoxZ5dRZqXRVox1xd9siIPE3e6CuVQIMabTp1YLno=|Y38qtTuCwNLDqFnzJ3Cgbjm1SE15OnhDm9iAMABaQBA=".parse().unwrap(); |
| 752 | + |
| 753 | + let request = VerifyAsymmetricKeysRequest { |
| 754 | + user_public_key: key_pair.public, |
| 755 | + user_key_encrypted_private_key: undecryptable_private_key, |
| 756 | + }; |
| 757 | + let response = client.crypto().verify_asymmetric_keys(request).unwrap(); |
| 758 | + |
| 759 | + assert!(!response.private_key_decryptable); |
| 760 | + assert!(!response.valid_private_key); |
| 761 | + } |
| 762 | + |
| 763 | + #[test] |
| 764 | + fn test_verify_asymmetric_keys_parse_failed() { |
| 765 | + let (client, user_key, key_pair) = setup_asymmetric_keys_test(); |
| 766 | + |
| 767 | + let invalid_private_key = "bad_key" |
| 768 | + .to_string() |
| 769 | + .into_bytes() |
| 770 | + .encrypt_with_key(&user_key.0) |
| 771 | + .unwrap(); |
| 772 | + |
| 773 | + let request = VerifyAsymmetricKeysRequest { |
| 774 | + user_public_key: key_pair.public, |
| 775 | + user_key_encrypted_private_key: invalid_private_key, |
| 776 | + }; |
| 777 | + let response = client.crypto().verify_asymmetric_keys(request).unwrap(); |
| 778 | + |
| 779 | + assert!(response.private_key_decryptable); |
| 780 | + assert!(!response.valid_private_key); |
| 781 | + } |
| 782 | + |
| 783 | + #[test] |
| 784 | + fn test_verify_asymmetric_keys_key_mismatch() { |
| 785 | + let (client, user_key, key_pair) = setup_asymmetric_keys_test(); |
| 786 | + let new_key_pair = user_key.make_key_pair().unwrap(); |
| 787 | + |
| 788 | + let request = VerifyAsymmetricKeysRequest { |
| 789 | + user_public_key: key_pair.public, |
| 790 | + user_key_encrypted_private_key: new_key_pair.private, |
| 791 | + }; |
| 792 | + let response = client.crypto().verify_asymmetric_keys(request).unwrap(); |
| 793 | + |
| 794 | + assert!(response.private_key_decryptable); |
| 795 | + assert!(!response.valid_private_key); |
| 796 | + } |
588 | 797 | }
|
0 commit comments