Skip to content

Commit

Permalink
Merge branch 'main' into sm/sm-1189
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas-Avery authored Apr 22, 2024
2 parents bb554c6 + a863e89 commit e49c976
Show file tree
Hide file tree
Showing 18 changed files with 276 additions and 139 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,5 +140,24 @@ VALUES
);
```

## Developer tools

This project recommends the use of certain developer tools, and also includes configurations for
them to make developers lives easier. The use of these tools is optional and they might require a
separate installation step.

The list of developer tools is:

- `Visual Studio Code`: We provide a recommended extension list which should show under the
`Extensions` tab when opening this project with the editor. We also offer a few launch settings
and tasks to build and run the SDK
- `bacon`: This is a CLI background code checker. We provide a configuration file with some of the
most common tasks to run (`check`, `clippy`, `test`, `doc` - run `bacon -l` to see them all). This
tool needs to be installed separately by running `cargo install bacon --locked`.
- `nexttest`: This is a new and faster test runner, capable of running tests in parallel and with a
much nicer output compared to `cargo test`. This tool needs to be installed separately by running
`cargo install cargo-nextest --locked`. It can be manually run using
`cargo nextest run --all-features`

[secrets-manager]: https://bitwarden.com/products/secrets-manager/
[bws-help]: https://bitwarden.com/help/secrets-manager-cli/
78 changes: 78 additions & 0 deletions bacon.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml

default_job = "check"

[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false

[jobs.check-all]
command = ["cargo", "check", "--all-targets", "--color", "always"]
need_stdout = false

[jobs.clippy]
command = ["cargo", "clippy", "--all-targets", "--color", "always"]
need_stdout = false

[jobs.test]
command = [
"cargo",
"test",
"--all-features",
"--color",
"always",
"--",
"--color",
"always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true

[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false

# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change

[jobs.doc-internal]
command = [
"cargo",
"doc",
"--color",
"always",
"--no-deps",
"--all-features",
"--document-private-items",
]
need_stdout = false

[jobs.doc-internal-open]
command = [
"cargo",
"doc",
"--color",
"always",
"--no-deps",
"--all-features",
"--document-private-items",
"--open",
]
allow_warnings = true
need_stdout = false
on_success = "job:doc-internal"

# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
9 changes: 9 additions & 0 deletions crates/bitwarden-crypto/src/keys/master_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use serde::{Deserialize, Serialize};
use super::utils::{derive_kdf_key, stretch_kdf_key};
use crate::{util, CryptoError, EncString, KeyDecryptable, Result, SymmetricCryptoKey, UserKey};

/// Key Derivation Function for Bitwarden Account
///
/// In Bitwarden accounts can use multiple KDFs to derive their master key from their password. This
/// Enum represents all the possible KDFs.
#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "mobile", derive(uniffi::Enum))]
Expand All @@ -22,22 +26,27 @@ pub enum Kdf {
}

impl Default for Kdf {
/// Default KDF for new accounts.
fn default() -> Self {
Kdf::PBKDF2 {
iterations: default_pbkdf2_iterations(),
}
}
}

/// Default PBKDF2 iterations
pub fn default_pbkdf2_iterations() -> NonZeroU32 {
NonZeroU32::new(600_000).expect("Non-zero number")
}
/// Default Argon2 iterations
pub fn default_argon2_iterations() -> NonZeroU32 {
NonZeroU32::new(3).expect("Non-zero number")
}
/// Default Argon2 memory
pub fn default_argon2_memory() -> NonZeroU32 {
NonZeroU32::new(64).expect("Non-zero number")
}
/// Default Argon2 parallelism
pub fn default_argon2_parallelism() -> NonZeroU32 {
NonZeroU32::new(4).expect("Non-zero number")
}
Expand Down
42 changes: 37 additions & 5 deletions crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,47 @@
//! # Bitwarden Cryptographic primitives
//!
//! This crate contains the cryptographic primitives used throughout the SDK. The crate makes a
//! best effort to abstract away cryptographic concepts into concepts such as [`EncString`],
//! [`AsymmetricEncString`] and [`SymmetricCryptoKey`].
//! This crate contains the cryptographic primitives used throughout the SDK. The general
//! aspiration is for this crate to handle all the difficult cryptographic operations and expose
//! higher level concepts to the rest of the SDK.
//!
//! ## Conventions:
//! <div class="warning">
//! Generally you should <b>not</b> find yourself needing to edit this crate! Everything written
//! here requires additional care and attention to ensure that the cryptographic primitives are
//! secure. </div>
//!
//! ## Example:
//!
//! ```rust
//! use bitwarden_crypto::{SymmetricCryptoKey, KeyEncryptable, KeyDecryptable, CryptoError};
//!
//! async fn example() -> Result<(), CryptoError> {
//! let key = SymmetricCryptoKey::generate(rand::thread_rng());
//!
//! let data = "Hello, World!".to_owned();
//! let encrypted = data.clone().encrypt_with_key(&key)?;
//! let decrypted: String = encrypted.decrypt_with_key(&key)?;
//!
//! assert_eq!(data, decrypted);
//! Ok(())
//! }
//! ```
//!
//! ## Development considerations
//!
//! This crate is expected to provide long term support for cryptographic operations. To that end,
//! the following considerations should be taken into account when making changes to this crate:
//!
//! - Limit public interfaces to the bare minimum.
//! - Breaking changes should be rare and well communicated.
//! - Serializable representation of keys and encrypted data must be supported indefinitely as we
//! have no way to update all data.
//!
//! ### Conventions:
//!
//! - Pure Functions that deterministically "derive" keys from input are prefixed with `derive_`.
//! - Functions that generate non deterministically keys are prefixed with `make_`.
//!
//! ## Differences from `clients`
//! ### Differences from `clients`
//!
//! There are some noteworthy differences compared to the other Bitwarden
//! [clients](https://github.com/bitwarden/clients). These changes are made in an effort to
Expand Down
2 changes: 2 additions & 0 deletions crates/bitwarden-crypto/src/rsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub struct RsaKeyPair {
pub private: EncString,
}

/// Generate a new RSA key pair of 2048 bits
pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result<RsaKeyPair> {
let mut rng = rand::thread_rng();
let bits = 2048;
Expand Down Expand Up @@ -48,6 +49,7 @@ pub(crate) fn make_key_pair(key: &SymmetricCryptoKey) -> Result<RsaKeyPair> {
})
}

/// Encrypt data using RSA-OAEP-SHA1 with a 2048 bit key
pub(super) fn encrypt_rsa2048_oaep_sha1(public_key: &RsaPublicKey, data: &[u8]) -> Result<Vec<u8>> {
let mut rng = rand::thread_rng();

Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/wordlist.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// EFF's Long Wordlist from https://www.eff.org/dice
/// EFF's Long Wordlist from <https://www.eff.org/dice>
pub const EFF_LONG_WORD_LIST: &[&str] = &[
"abacus",
"abdomen",
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-exporters/src/csv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub(crate) fn export_csv(folders: Vec<Folder>, ciphers: Vec<Cipher>) -> Result<S
String::from_utf8(wtr.into_inner().map_err(|_| CsvError::Csv)?).map_err(|_| CsvError::Csv)
}

/// CSV export format. See https://bitwarden.com/help/condition-bitwarden-import/#condition-a-csv
/// CSV export format. See <https://bitwarden.com/help/condition-bitwarden-import/#condition-a-csv>
///
/// Be careful when changing this struct to maintain compatibility with old exports.
#[derive(serde::Serialize)]
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-generators/src/username.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ fn random_number(mut rng: impl RngCore) -> String {
}

/// Generate a username using a plus addressed email address
/// The format is <username>+<random-or-website>@<domain>
/// The format is `<username>+<random-or-website>@<domain>`
fn username_subaddress(mut rng: impl RngCore, r#type: AppendType, email: String) -> String {
if email.len() < 3 {
return email;
Expand All @@ -195,7 +195,7 @@ fn username_subaddress(mut rng: impl RngCore, r#type: AppendType, email: String)
}

/// Generate a username using a catchall email address
/// The format is <random-or-website>@<domain>
/// The format is `<random-or-website>@<domain>`
fn username_catchall(mut rng: impl RngCore, r#type: AppendType, domain: String) -> String {
if domain.is_empty() {
return domain;
Expand Down
3 changes: 0 additions & 3 deletions crates/bitwarden-napi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,5 @@ napi-derive = "2"
[build-dependencies]
napi-build = "2.1.0"

[profile.release]
lto = true

[lints]
workspace = true
30 changes: 14 additions & 16 deletions crates/bitwarden/src/auth/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,26 +125,25 @@ mod tests {
use bitwarden_crypto::Kdf;

use super::*;
use crate::{
client::{LoginMethod, UserLoginMethod},
mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest},
};
use crate::mobile::crypto::{AuthRequestMethod, InitUserCryptoMethod, InitUserCryptoRequest};

#[test]
fn test_approve() {
let mut client = Client::new(None);
client.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "7b821276-e27c-400b-9853-606393c87f18".to_owned(),
email: "[email protected]".to_owned(),
kdf: Kdf::PBKDF2 {

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

let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();
let private_key ="2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=".parse().unwrap();
client
.initialize_user_crypto("asdfasdfasdf", user_key, private_key)
.initialize_user_crypto_master_key(master_key, user_key, private_key)
.unwrap();

let public_key = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvyLRDUwXB4BfQ507D4meFPmwn5zwy3IqTPJO4plrrhnclWahXa240BzyFW9gHgYu+Jrgms5xBfRTBMcEsqqNm7+JpB6C1B6yvnik0DpJgWQw1rwvy4SUYidpR/AWbQi47n/hvnmzI/sQxGddVfvWu1iTKOlf5blbKYAXnUE5DZBGnrWfacNXwRRdtP06tFB0LwDgw+91CeLSJ9py6dm1qX5JIxoO8StJOQl65goLCdrTWlox+0Jh4xFUfCkb+s3px+OhSCzJbvG/hlrSRcUz5GnwlCEyF3v5lfUtV96MJD+78d8pmH6CfFAp2wxKRAbGdk+JccJYO6y6oIXd3Fm7twIDAQAB";
Expand Down Expand Up @@ -206,14 +205,13 @@ mod tests {

// Initialize an existing client which is unlocked
let mut existing_device = Client::new(None);
existing_device.set_login_method(LoginMethod::User(UserLoginMethod::Username {
client_id: "123".to_owned(),
email: email.to_owned(),
kdf: kdf.clone(),
}));

let master_key =
bitwarden_crypto::MasterKey::derive("asdfasdfasdf".as_bytes(), email.as_bytes(), &kdf)
.unwrap();

existing_device
.initialize_user_crypto("asdfasdfasdf", user_key, private_key.parse().unwrap())
.initialize_user_crypto_master_key(master_key, user_key, private_key.parse().unwrap())
.unwrap();

// Initialize a new device which will request to be logged in
Expand Down
7 changes: 5 additions & 2 deletions crates/bitwarden/src/auth/login/api_key.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use bitwarden_crypto::EncString;
use bitwarden_crypto::{EncString, MasterKey};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -37,6 +37,9 @@ pub(crate) async fn login_api_key(
r.refresh_token.clone(),
r.expires_in,
);

let master_key = MasterKey::derive(input.password.as_bytes(), email.as_bytes(), &kdf)?;

client.set_login_method(LoginMethod::User(UserLoginMethod::ApiKey {
client_id: input.client_id.to_owned(),
client_secret: input.client_secret.to_owned(),
Expand All @@ -47,7 +50,7 @@ pub(crate) async fn login_api_key(
let user_key: EncString = require!(r.key.as_deref()).parse()?;
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;

client.initialize_user_crypto(&input.password, user_key, private_key)?;
client.initialize_user_crypto_master_key(master_key, user_key, private_key)?;
}

ApiKeyLoginResponse::process_response(response)
Expand Down
19 changes: 10 additions & 9 deletions crates/bitwarden/src/auth/login/password.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#[cfg(feature = "internal")]
use log::{debug, info};
use log::info;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

Expand All @@ -22,19 +22,20 @@ pub(crate) async fn login_password(
client: &mut Client,
input: &PasswordLoginRequest,
) -> Result<PasswordLoginResponse> {
use bitwarden_crypto::{EncString, HashPurpose};
use bitwarden_crypto::{EncString, HashPurpose, MasterKey};

use crate::{auth::determine_password_hash, client::UserLoginMethod, error::require};
use crate::{client::UserLoginMethod, error::require};

info!("password logging in");
debug!("{:#?}, {:#?}", client, input);

let password_hash = determine_password_hash(
&input.email,
let master_key = MasterKey::derive(
input.password.as_bytes(),
input.email.as_bytes(),
&input.kdf,
&input.password,
HashPurpose::ServerAuthorization,
)?;
let password_hash = master_key
.derive_master_key_hash(input.password.as_bytes(), HashPurpose::ServerAuthorization)?;

let response = request_identity_tokens(client, input, &password_hash).await?;

if let IdentityTokenResponse::Authenticated(r) = &response {
Expand All @@ -52,7 +53,7 @@ pub(crate) async fn login_password(
let user_key: EncString = require!(r.key.as_deref()).parse()?;
let private_key: EncString = require!(r.private_key.as_deref()).parse()?;

client.initialize_user_crypto(&input.password, user_key, private_key)?;
client.initialize_user_crypto_master_key(master_key, user_key, private_key)?;
}

PasswordLoginResponse::process_response(response)
Expand Down
Loading

0 comments on commit e49c976

Please sign in to comment.