Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-12400] Add private key regeneration SDK methods #6

Merged
merged 7 commits into from
Dec 11, 2024
16 changes: 15 additions & 1 deletion crates/bitwarden-core/src/mobile/client_crypto.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
#[cfg(feature = "internal")]
use bitwarden_crypto::{AsymmetricEncString, EncString};

use super::crypto::{derive_key_connector, DeriveKeyConnectorRequest};
use super::crypto::{
derive_key_connector, make_key_pair, verify_asymmetric_keys, DeriveKeyConnectorRequest,
MakeKeyPairResponse, VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
};
use crate::{client::encryption_settings::EncryptionSettingsError, Client};
#[cfg(feature = "internal")]
use crate::{
Expand Down Expand Up @@ -56,6 +59,17 @@
pub fn derive_key_connector(&self, request: DeriveKeyConnectorRequest) -> Result<String> {
derive_key_connector(request)
}

pub fn make_key_pair(&self, user_key: String) -> Result<MakeKeyPairResponse> {
make_key_pair(user_key)
}

Check warning on line 65 in crates/bitwarden-core/src/mobile/client_crypto.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/mobile/client_crypto.rs#L63-L65

Added lines #L63 - L65 were not covered by tests

pub fn verify_asymmetric_keys(
&self,
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse> {
verify_asymmetric_keys(request)
}

Check warning on line 72 in crates/bitwarden-core/src/mobile/client_crypto.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-core/src/mobile/client_crypto.rs#L67-L72

Added lines #L67 - L72 were not covered by tests
}

impl<'a> Client {
Expand Down
205 changes: 203 additions & 2 deletions crates/bitwarden-core/src/mobile/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use std::collections::HashMap;

use base64::{engine::general_purpose::STANDARD, Engine};
use bitwarden_crypto::{
AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable, MasterKey,
SymmetricCryptoKey,
AsymmetricCryptoKey, AsymmetricEncString, EncString, Kdf, KeyDecryptable, KeyEncryptable,
MasterKey, SymmetricCryptoKey, UserKey,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -350,10 +351,115 @@
Ok(master_key.to_base64())
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]

Check warning on line 354 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L354 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]

Check warning on line 357 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L357 was not covered by tests
pub struct MakeKeyPairResponse {
/// The user's public key
user_public_key: String,
/// User's private key, encrypted with the user key
user_key_encrypted_private_key: EncString,
}

pub fn make_key_pair(user_key: String) -> Result<MakeKeyPairResponse> {
let user_key = UserKey::new(SymmetricCryptoKey::try_from(user_key)?);

let key_pair = user_key.make_key_pair()?;

Ok(MakeKeyPairResponse {
user_public_key: key_pair.public,
user_key_encrypted_private_key: key_pair.private,
})
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]

Check warning on line 376 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L376 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]

Check warning on line 379 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L379 was not covered by tests
pub struct VerifyAsymmetricKeysRequest {
/// The user's user key
user_key: String,
/// The user's public key
user_public_key: String,
/// User's private key, encrypted with the user key
user_key_encrypted_private_key: EncString,
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]

Check warning on line 389 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L389 was not covered by tests
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]

Check warning on line 392 in crates/bitwarden-core/src/mobile/crypto.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L392 was not covered by tests
pub struct VerifyAsymmetricKeysResponse {
/// Whether the user's private key was decryptable by the user key.
private_key_decryptable: bool,
/// Whether the user's private key was a valid RSA key and matched the public key provided.
valid_private_key: bool,
}

pub fn verify_asymmetric_keys(
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse> {
#[derive(Debug, thiserror::Error)]
enum VerifyError {
#[error("Failed to decrypt private key: {0:?}")]
DecryptFailed(bitwarden_crypto::CryptoError),
#[error("Failed to parse decrypted private key: {0:?}")]
ParseFailed(bitwarden_crypto::CryptoError),
#[error("Failed to derive a public key: {0:?}")]
PublicFailed(bitwarden_crypto::CryptoError),
#[error("Derived public key doesn't match")]
KeyMismatch,
}

fn verify_inner(
user_key: &SymmetricCryptoKey,
request: &VerifyAsymmetricKeysRequest,
) -> Result<(), VerifyError> {
let decrypted_private_key: Vec<u8> = request
.user_key_encrypted_private_key
.decrypt_with_key(user_key)
.map_err(VerifyError::DecryptFailed)?;

let private_key = AsymmetricCryptoKey::from_der(&decrypted_private_key)
.map_err(VerifyError::ParseFailed)?;

let derived_public_key_vec = private_key
.to_public_der()
.map_err(VerifyError::PublicFailed)?;

let derived_public_key = STANDARD.encode(&derived_public_key_vec);

if derived_public_key != request.user_public_key {
return Err(VerifyError::KeyMismatch);
}
Ok(())
}

let user_key = SymmetricCryptoKey::try_from(request.user_key.clone())?;

Ok(match verify_inner(&user_key, &request) {
Ok(_) => VerifyAsymmetricKeysResponse {
private_key_decryptable: true,
valid_private_key: true,
},
Err(e) => {
log::debug!("User asymmetric keys verification: {}", e);

VerifyAsymmetricKeysResponse {
private_key_decryptable: !matches!(e, VerifyError::DecryptFailed(_)),
valid_private_key: false,
}
}
})
}

#[cfg(test)]
mod tests {
use std::num::NonZeroU32;

use bitwarden_crypto::RsaKeyPair;

use super::*;
use crate::Client;

Expand Down Expand Up @@ -585,4 +691,99 @@

assert_eq!(result, "ySXq1RVLKEaV1eoQE/ui9aFKIvXTl9PAXwp1MljfF50=");
}

fn setup_asymmetric_keys_test() -> (UserKey, RsaKeyPair) {
let master_key = MasterKey::derive(
"asdfasdfasdf",
"test@bitwarden.com",
&Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
)
.unwrap();
let user_key = (master_key.make_user_key().unwrap()).0;
let key_pair = user_key.make_key_pair().unwrap();

(user_key, key_pair)
}

#[test]
fn test_make_key_pair() {
let (user_key, _) = setup_asymmetric_keys_test();

let response = make_key_pair(user_key.0.to_base64()).unwrap();

assert!(!response.user_public_key.is_empty());
let encrypted_private_key = response.user_key_encrypted_private_key;
let private_key: Vec<u8> = encrypted_private_key.decrypt_with_key(&user_key.0).unwrap();
assert!(!private_key.is_empty());
}

#[test]
fn test_verify_asymmetric_keys_success() {
let (user_key, key_pair) = setup_asymmetric_keys_test();

let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: key_pair.private,
};
let response = verify_asymmetric_keys(request).unwrap();

assert!(response.private_key_decryptable);
assert!(response.valid_private_key);
}

#[test]
fn test_verify_asymmetric_keys_decrypt_failed() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
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();

let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: undecryptable_private_key,
};
let response = verify_asymmetric_keys(request).unwrap();

assert!(!response.private_key_decryptable);
assert!(!response.valid_private_key);
}

#[test]
fn test_verify_asymmetric_keys_parse_failed() {
let (user_key, key_pair) = setup_asymmetric_keys_test();

let invalid_private_key = "bad_key"
.to_string()
.into_bytes()
.encrypt_with_key(&user_key.0)
.unwrap();

let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: invalid_private_key,
};
let response = verify_asymmetric_keys(request).unwrap();

assert!(response.private_key_decryptable);
assert!(!response.valid_private_key);
}

#[test]
fn test_verify_asymmetric_keys_key_mismatch() {
let (user_key, key_pair) = setup_asymmetric_keys_test();
let new_key_pair = user_key.make_key_pair().unwrap();

let request = VerifyAsymmetricKeysRequest {
user_key: user_key.0.to_base64(),
user_public_key: key_pair.public,
user_key_encrypted_private_key: new_key_pair.private,
};
let response = verify_asymmetric_keys(request).unwrap();

assert!(response.private_key_decryptable);
assert!(!response.valid_private_key);
}
}
24 changes: 23 additions & 1 deletion crates/bitwarden-wasm-internal/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

use bitwarden_core::{
client::encryption_settings::EncryptionSettingsError,
mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest},
mobile::crypto::{
InitOrgCryptoRequest, InitUserCryptoRequest, MakeKeyPairResponse,
VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
},
Client,
};
use wasm_bindgen::prelude::*;
Expand Down Expand Up @@ -35,4 +38,23 @@
) -> Result<(), EncryptionSettingsError> {
self.0.crypto().initialize_org_crypto(req).await
}

/// Generates a new key pair and encrypts the private key with the provided user key.
/// Crypto initialization not required.
pub fn make_key_pair(
&self,
user_key: String,
) -> Result<MakeKeyPairResponse, bitwarden_core::Error> {
self.0.crypto().make_key_pair(user_key)
}

Check warning on line 49 in crates/bitwarden-wasm-internal/src/crypto.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-wasm-internal/src/crypto.rs#L44-L49

Added lines #L44 - L49 were not covered by tests

/// Verifies a user's asymmetric keys by decrypting the private key with the provided user
/// key. Returns if the private key is decryptable and if it is a valid matching key.
/// Crypto initialization not required.
pub fn verify_asymmetric_keys(
&self,
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse, bitwarden_core::Error> {
self.0.crypto().verify_asymmetric_keys(request)
}

Check warning on line 59 in crates/bitwarden-wasm-internal/src/crypto.rs

View check run for this annotation

Codecov / codecov/patch

crates/bitwarden-wasm-internal/src/crypto.rs#L54-L59

Added lines #L54 - L59 were not covered by tests
}
Loading