Skip to content

Commit 98c0d0e

Browse files
committed
add private key regen SDK methods
1 parent 3800954 commit 98c0d0e

File tree

3 files changed

+246
-4
lines changed

3 files changed

+246
-4
lines changed

crates/bitwarden-core/src/mobile/client_crypto.rs

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
#[cfg(feature = "internal")]
22
use bitwarden_crypto::{AsymmetricEncString, EncString};
33

4-
use super::crypto::{derive_key_connector, DeriveKeyConnectorRequest};
4+
use super::crypto::{
5+
derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorRequest,
6+
MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
7+
};
58
use crate::{client::encryption_settings::EncryptionSettingsError, Client};
69
#[cfg(feature = "internal")]
710
use crate::{
@@ -56,6 +59,17 @@ impl<'a> ClientCrypto<'a> {
5659
pub fn derive_key_connector(&self, request: DeriveKeyConnectorRequest) -> Result<String> {
5760
derive_key_connector(request)
5861
}
62+
63+
pub fn make_key_pair(&self) -> Result<MakeKeyPairResponse> {
64+
make_key_pair(self.client)
65+
}
66+
67+
pub fn verify_asymmetric_keys(
68+
&self,
69+
request: VerifyAsymmetricKeysRequest,
70+
) -> Result<VerifyAsymmetricKeysResponse> {
71+
verify_asymmetric_keys(self.client, request)
72+
}
5973
}
6074

6175
impl<'a> Client {

crates/bitwarden-core/src/mobile/crypto.rs

+211-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use std::collections::HashMap;
22

3+
use base64::{engine::general_purpose::STANDARD, Engine};
34
use bitwarden_crypto::{
4-
AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey,
5-
SymmetricCryptoKey,
5+
AsymmetricCryptoKey, AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable,
6+
MasterKey, SymmetricCryptoKey, UserKey,
67
};
78
use schemars::JsonSchema;
89
use serde::{Deserialize, Serialize};
@@ -350,10 +351,117 @@ pub(super) fn derive_key_connector(request: DeriveKeyConnectorRequest) -> Result
350351
Ok(master_key.to_base64())
351352
}
352353

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+
353459
#[cfg(test)]
354460
mod tests {
355461
use std::num::NonZeroU32;
356462

463+
use bitwarden_crypto::RsaKeyPair;
464+
357465
use super::*;
358466
use crate::Client;
359467

@@ -585,4 +693,105 @@ mod tests {
585693

586694
assert_eq!(result, "ySXq1RVLKEaV1eoQE/ui9aFKIvXTl9PAXwp1MljfF50=");
587695
}
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+
}
588797
}

crates/bitwarden-wasm-internal/src/crypto.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
use std::rc::Rc;
22

33
use bitwarden_core::{
4-
mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest},
4+
mobile::crypto::{
5+
InitOrgCryptoRequest, InitUserCryptoRequest, MakeKeyPairResponse,
6+
VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
7+
},
58
Client,
69
};
710
use wasm_bindgen::prelude::*;
@@ -30,4 +33,20 @@ impl ClientCrypto {
3033
pub async fn initialize_org_crypto(&self, req: InitOrgCryptoRequest) -> Result<()> {
3134
Ok(self.0.crypto().initialize_org_crypto(req).await?)
3235
}
36+
37+
/// Generates a new key pair and encrypts the private key with the initialized user key.
38+
/// Needs to be called after `initialize_user_crypto`.
39+
pub fn make_key_pair(&self) -> Result<MakeKeyPairResponse> {
40+
Ok(self.0.crypto().make_key_pair()?)
41+
}
42+
43+
/// Verifies a user's asymmetric keys by decrypting the private key with the initialized user
44+
/// key. Returns if the private key is decryptable and if it is a valid matching key.
45+
/// Needs to be called after `initialize_user_crypto`.
46+
pub fn verify_asymmetric_keys(
47+
&self,
48+
request: VerifyAsymmetricKeysRequest,
49+
) -> Result<VerifyAsymmetricKeysResponse> {
50+
Ok(self.0.crypto().verify_asymmetric_keys(request)?)
51+
}
3352
}

0 commit comments

Comments
 (0)