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 #1133

Closed
wants to merge 24 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6fb114b
feat(wip): add client crypto to client
coroiu Oct 7, 2024
0857aa6
feat: implement wasm error handling
coroiu Oct 7, 2024
8670757
feat: add support for converting core errors
coroiu Oct 7, 2024
8860dfd
feat: add error handling to `ClientCrypto` calls
coroiu Oct 7, 2024
bbaccba
fix: most of the missing bitwarden-crypto types
coroiu Oct 7, 2024
94e9510
fix: add type for uuid, not sure if this will work
coroiu Oct 7, 2024
30dac65
fix: `AuthRequestMethod` missing in types
coroiu Oct 7, 2024
1d1644f
fix: editorconfig not being picked up by vsocde
coroiu Oct 9, 2024
02ec621
refactor: move external TS types into wasm crate
coroiu Oct 9, 2024
a1201f1
feat: add support for decrypting folders
coroiu Oct 9, 2024
e23e30b
lint: run cargo fmt
coroiu Oct 10, 2024
79505d4
fix: node linting
coroiu Oct 10, 2024
0105672
lint: more prettier linting......
coroiu Oct 10, 2024
8452d66
fix: move NonZeroU32 to custom_types
coroiu Oct 10, 2024
5c6bcda
revert: .editorconfig changes
coroiu Oct 10, 2024
5ee793a
fix: snippet node compatiblity
coroiu Oct 11, 2024
ac0059b
Add private key regen SDK methods
Thomas-Avery Oct 11, 2024
0997cd9
Merge branch 'PM-11764-implement-account-switching-and-sdk-initializaโ€ฆ
Thomas-Avery Oct 11, 2024
963e9af
clippy fixes
Thomas-Avery Oct 11, 2024
32a4690
Merge branch 'main' into km/pm-12400/private-key-regen
Thomas-Avery Oct 14, 2024
c3be870
delete error.js
Thomas-Avery Oct 14, 2024
2db787d
Add methods to wasm-internal
Thomas-Avery Oct 14, 2024
e9d4fc2
Add code comments to public API.
Thomas-Avery Oct 16, 2024
a032535
Merge branch 'main' into km/pm-12400/private-key-regen
Thomas-Avery Oct 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ impl<'a> ClientCrypto<'a> {
pub fn derive_key_connector(&self, request: DeriveKeyConnectorRequest) -> Result<String> {
derive_key_connector(request)
}

pub fn make_key_pair(&self) -> Result<MakeKeyPairResponse> {
make_key_pair(self.client)
}

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

impl<'a> Client {
Expand Down
213 changes: 211 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,117 @@ pub(super) fn derive_key_connector(request: DeriveKeyConnectorRequest) -> Result
Ok(master_key.to_base64())
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[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))]
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(client: &Client) -> Result<MakeKeyPairResponse> {
let enc = client.internal.get_encryption_settings()?;
let user_key = UserKey::new(enc.get_key(&None)?.clone());

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)]
#[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))]
pub struct VerifyAsymmetricKeysRequest {
/// 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)]
#[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))]
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(
client: &Client,
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 enc = client.internal.get_encryption_settings()?;
let user_key = enc.get_key(&None)?;

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 +693,105 @@ mod tests {

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

fn setup_asymmetric_keys_test() -> (Client, UserKey, RsaKeyPair) {
let client = Client::new(None);

let master_key = MasterKey::derive(
"asdfasdfasdf",
"[email protected]",
&Kdf::PBKDF2 {
iterations: NonZeroU32::new(600_000).unwrap(),
},
)
.unwrap();
let (user_key, encrypted_user_key) = master_key.make_user_key().unwrap();
let key_pair = user_key.make_key_pair().unwrap();

client
.internal
.initialize_user_crypto_master_key(
master_key,
encrypted_user_key,
key_pair.private.clone(),
)
.unwrap();
(client, user_key, key_pair)
}

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

let response = make_key_pair(&client).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 (client, _, key_pair) = setup_asymmetric_keys_test();

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

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

#[test]
fn test_verify_asymmetric_keys_decrypt_failed() {
let (client, _, 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_public_key: key_pair.public,
user_key_encrypted_private_key: undecryptable_private_key,
};
let response = client.crypto().verify_asymmetric_keys(request).unwrap();

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

#[test]
fn test_verify_asymmetric_keys_parse_failed() {
let (client, 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_public_key: key_pair.public,
user_key_encrypted_private_key: invalid_private_key,
};
let response = client.crypto().verify_asymmetric_keys(request).unwrap();

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

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

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

assert!(response.private_key_decryptable);
assert!(!response.valid_private_key);
}
}
21 changes: 20 additions & 1 deletion crates/bitwarden-wasm-internal/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::rc::Rc;

use bitwarden_core::{
mobile::crypto::{InitOrgCryptoRequest, InitUserCryptoRequest},
mobile::crypto::{
InitOrgCryptoRequest, InitUserCryptoRequest, MakeKeyPairResponse,
VerifyAsymmetricKeysRequest, VerifyAsymmetricKeysResponse,
},
Client,
};
use wasm_bindgen::prelude::*;
Expand Down Expand Up @@ -30,4 +33,20 @@ impl ClientCrypto {
pub async fn initialize_org_crypto(&self, req: InitOrgCryptoRequest) -> Result<()> {
Ok(self.0.crypto().initialize_org_crypto(req).await?)
}

/// Generates a new key pair and encrypts the private key with the initialized user key.
/// Needs to be called after `initialize_user_crypto`.
pub fn make_key_pair(&self) -> Result<MakeKeyPairResponse> {
Thomas-Avery marked this conversation as resolved.
Show resolved Hide resolved
Ok(self.0.crypto().make_key_pair()?)
}

/// Verifies a user's asymmetric keys by decrypting the private key with the initialized user
/// key. Returns if the private key is decryptable and if it is a valid matching key.
/// Needs to be called after `initialize_user_crypto`.
pub fn verify_asymmetric_keys(
&self,
request: VerifyAsymmetricKeysRequest,
) -> Result<VerifyAsymmetricKeysResponse> {
Ok(self.0.crypto().verify_asymmetric_keys(request)?)
}
}
Loading