diff --git a/Cargo.lock b/Cargo.lock index 21e2fd573..92e9294a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -53,6 +53,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "afl" +version = "0.15.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e868a49dcc54f7edcec970721ba68c4b5cc5f1e478393ae2dd2d475efd752e" +dependencies = [ + "home", + "libc", + "rustc_version", + "xdg", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -156,6 +168,15 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "argon2" version = "0.5.3" @@ -653,6 +674,7 @@ dependencies = [ name = "bitwarden-vault" version = "1.0.0" dependencies = [ + "arbitrary", "base64", "bitwarden-api-api", "bitwarden-core", @@ -824,6 +846,8 @@ version = "1.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -1367,6 +1391,17 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -1681,6 +1716,19 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzz" +version = "0.0.0" +dependencies = [ + "afl", + "bitwarden-crypto", + "bitwarden-exporters", + "bitwarden-fido", + "bitwarden-vault", + "chrono", + "libfuzzer-sys", +] + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -1876,6 +1924,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "1.3.1" @@ -2322,6 +2379,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.2", + "libc", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -2347,6 +2414,16 @@ version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.6" @@ -5130,6 +5207,12 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 88ba530c5..8f7ed3fb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["bitwarden_license/*", "crates/*"] +members = ["bitwarden_license/*", "crates/*", "fuzz"] # Global settings for all crates should be defined here [workspace.package] @@ -35,6 +35,8 @@ bitwarden-vault = { path = "crates/bitwarden-vault", version = "=1.0.0" } bitwarden-error = { path = "crates/bitwarden-error", version = "=1.0.0" } bitwarden-error-macro = { path = "crates/bitwarden-error-macro", version = "=1.0.0" } +fuzz = { path = "fuzz", version = "=0.0.0" } + # External crates that are expected to maintain a consistent version across all crates chrono = { version = ">=0.4.26, <0.5", features = [ "clock", diff --git a/crates/bitwarden-crypto/src/aes.rs b/crates/bitwarden-crypto/src/aes.rs index b890d92c5..1c01ed669 100644 --- a/crates/bitwarden-crypto/src/aes.rs +++ b/crates/bitwarden-crypto/src/aes.rs @@ -91,7 +91,7 @@ fn encrypt_aes256_internal( } /// Generate a MAC using HMAC-SHA256. -fn generate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { +pub fn generate_mac(mac_key: &[u8], iv: &[u8], data: &[u8]) -> Result<[u8; 32]> { let mut hmac = PbkdfSha256Hmac::new_from_slice(mac_key).expect("hmac new_from_slice should not fail"); hmac.update(iv); diff --git a/crates/bitwarden-crypto/src/enc_string/symmetric.rs b/crates/bitwarden-crypto/src/enc_string/symmetric.rs index 33d287e58..86b861904 100644 --- a/crates/bitwarden-crypto/src/enc_string/symmetric.rs +++ b/crates/bitwarden-crypto/src/enc_string/symmetric.rs @@ -351,6 +351,12 @@ mod tests { KEY_ID_SIZE, }; + #[test] + fn from_buffer() { + let a = EncString::from_buffer(&[7]); + println!("{:?}", a); + } + #[test] fn test_enc_roundtrip_xchacha20() { let key_id = [0u8; KEY_ID_SIZE]; diff --git a/crates/bitwarden-crypto/src/lib.rs b/crates/bitwarden-crypto/src/lib.rs index d3a0e304d..8a950e088 100644 --- a/crates/bitwarden-crypto/src/lib.rs +++ b/crates/bitwarden-crypto/src/lib.rs @@ -13,6 +13,7 @@ static ALLOC: ZeroizingAllocator = ZeroizingAllocator(std::alloc::System); mod aes; +pub use aes::generate_mac; mod enc_string; pub use enc_string::{EncString, UnsignedSharedKey}; mod error; diff --git a/crates/bitwarden-exporters/src/cxf/import.rs b/crates/bitwarden-exporters/src/cxf/import.rs index 67a73a581..0e433b385 100644 --- a/crates/bitwarden-exporters/src/cxf/import.rs +++ b/crates/bitwarden-exporters/src/cxf/import.rs @@ -9,7 +9,7 @@ use crate::{ CipherType, ImportingCipher, }; -pub(crate) fn parse_cxf(payload: String) -> Result, CxfError> { +pub fn parse_cxf(payload: String) -> Result, CxfError> { let account: CxfAccount = serde_json::from_str(&payload)?; let items: Vec = account.items.into_iter().flat_map(parse_item).collect(); diff --git a/crates/bitwarden-exporters/src/cxf/mod.rs b/crates/bitwarden-exporters/src/cxf/mod.rs index 21f5f40b5..a0c4007bd 100644 --- a/crates/bitwarden-exporters/src/cxf/mod.rs +++ b/crates/bitwarden-exporters/src/cxf/mod.rs @@ -11,6 +11,6 @@ mod export; pub(crate) use export::build_cxf; pub use export::Account; mod import; -pub(crate) use import::parse_cxf; +pub use import::parse_cxf; mod card; mod login; diff --git a/crates/bitwarden-exporters/src/lib.rs b/crates/bitwarden-exporters/src/lib.rs index 634d604c2..e664129f5 100644 --- a/crates/bitwarden-exporters/src/lib.rs +++ b/crates/bitwarden-exporters/src/lib.rs @@ -13,7 +13,7 @@ mod uniffi_support; mod csv; mod cxf; -pub use cxf::Account; +pub use cxf::{parse_cxf, Account}; mod encrypted_json; mod exporter_client; mod json; diff --git a/crates/bitwarden-fido/src/lib.rs b/crates/bitwarden-fido/src/lib.rs index 3c20b983a..dfb09f75b 100644 --- a/crates/bitwarden-fido/src/lib.rs +++ b/crates/bitwarden-fido/src/lib.rs @@ -105,7 +105,9 @@ impl TryFrom for Passkey { } } -fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result { +pub fn try_from_credential_full_view( + value: Fido2CredentialFullView, +) -> Result { let counter: u32 = value.counter.parse().expect("Invalid counter"); let counter = (counter != 0).then_some(counter); let key_value = URL_SAFE_NO_PAD.decode(value.key_value)?; diff --git a/crates/bitwarden-generators/src/lib.rs b/crates/bitwarden-generators/src/lib.rs index adb7ed035..5a121fec0 100644 --- a/crates/bitwarden-generators/src/lib.rs +++ b/crates/bitwarden-generators/src/lib.rs @@ -6,7 +6,7 @@ pub use generator_client::{GeneratorClient, GeneratorClientsExt}; pub(crate) mod passphrase; pub use passphrase::{PassphraseError, PassphraseGeneratorRequest}; pub(crate) mod password; -pub use password::{PasswordError, PasswordGeneratorRequest}; +pub use password::{password, PasswordError, PasswordGeneratorOptions, PasswordGeneratorRequest}; pub(crate) mod username; pub use username::{ForwarderServiceType, UsernameError, UsernameGeneratorRequest}; mod util; diff --git a/crates/bitwarden-generators/src/password.rs b/crates/bitwarden-generators/src/password.rs index bc3bbcfc2..127cebb64 100644 --- a/crates/bitwarden-generators/src/password.rs +++ b/crates/bitwarden-generators/src/password.rs @@ -130,7 +130,7 @@ impl Distribution for CharSet { /// Represents a set of valid options to generate a password with. /// To get an instance of it, use /// [`PasswordGeneratorRequest::validate_options`](PasswordGeneratorRequest::validate_options) -struct PasswordGeneratorOptions { +pub struct PasswordGeneratorOptions { pub(super) lower: (CharSet, usize), pub(super) upper: (CharSet, usize), pub(super) number: (CharSet, usize), @@ -224,7 +224,7 @@ impl PasswordGeneratorRequest { } /// Implementation of the random password generator. -pub(crate) fn password(input: PasswordGeneratorRequest) -> Result { +pub fn password(input: PasswordGeneratorRequest) -> Result { let options = input.validate_options()?; Ok(password_with_rng(rand::thread_rng(), options)) } diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 3073c1cd4..53eac75f4 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,6 +23,7 @@ uniffi = [ wasm = ["dep:tsify-next", "dep:wasm-bindgen"] # WASM support [dependencies] +arbitrary = { version = "1.4.1", features = ["derive_arbitrary"] } base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-core = { workspace = true, features = ["internal"] } diff --git a/crates/bitwarden-vault/src/cipher/login.rs b/crates/bitwarden-vault/src/cipher/login.rs index 07591c1ca..1ef40f585 100644 --- a/crates/bitwarden-vault/src/cipher/login.rs +++ b/crates/bitwarden-vault/src/cipher/login.rs @@ -5,7 +5,7 @@ use bitwarden_core::{ require, }; use bitwarden_crypto::{CryptoError, Decryptable, EncString, Encryptable, KeyStoreContext}; -use chrono::{DateTime, Utc}; +use chrono::{Date, DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; #[cfg(feature = "wasm")] @@ -151,6 +151,26 @@ pub struct Fido2CredentialFullView { pub creation_date: DateTime, } +impl<'a> arbitrary::Arbitrary<'a> for Fido2CredentialFullView { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Fido2CredentialFullView { + credential_id: u.arbitrary()?, + key_type: u.arbitrary()?, + key_algorithm: u.arbitrary()?, + key_curve: u.arbitrary()?, + key_value: u.arbitrary()?, + rp_id: u.arbitrary()?, + user_handle: u.arbitrary()?, + user_name: u.arbitrary()?, + counter: u.arbitrary()?, + rp_name: u.arbitrary()?, + user_display_name: u.arbitrary()?, + discoverable: u.arbitrary()?, + creation_date: DateTime::::from_timestamp_nanos(u.arbitrary::()?), + }) + } +} + // This is mostly a copy of the Fido2CredentialView, meant to be exposed to the clients // to let them select where to store the new credential. Note that it doesn't contain // the encrypted key as that is only filled when the cipher is selected diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 000000000..1a45eee77 --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..ab35dd1ab --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +bitwarden-crypto = { workspace = true } +bitwarden-exporters = { workspace = true } +bitwarden-fido = { workspace = true } +bitwarden-vault = { workspace = true } +chrono = { workspace = true } +afl = "0.15.19" + +[[bin]] +name = "parse_enc_buffer" +path = "fuzz_targets/parse_enc_buffer.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "parse_enc_string" +path = "fuzz_targets/parse_enc_string.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "parse_symmetric_crypto_key" +path = "fuzz_targets/parse_symmetric_crypto_key.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "parse_unsigned_shared_key" +path = "fuzz_targets/parse_unsigned_shared_key.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "parse_cxf" +path = "fuzz_targets/parse_cxf.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "parse_passkey" +path = "fuzz_targets/parse_passkey.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "generate_mac" +path = "fuzz_targets/generate_mac.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/generate_mac.rs b/fuzz/fuzz_targets/generate_mac.rs new file mode 100644 index 000000000..844cba86b --- /dev/null +++ b/fuzz/fuzz_targets/generate_mac.rs @@ -0,0 +1,9 @@ +#![no_main] + +use bitwarden_crypto::generate_mac; +use libfuzzer_sys::fuzz_target; + +// EncString parsing should never panic +fuzz_target!(|data: &[u8]| { + let _ = generate_mac(data, &[], &[]); +}); diff --git a/fuzz/fuzz_targets/parse_cxf.rs b/fuzz/fuzz_targets/parse_cxf.rs new file mode 100644 index 000000000..d1d2b0d39 --- /dev/null +++ b/fuzz/fuzz_targets/parse_cxf.rs @@ -0,0 +1,14 @@ +#![no_main] + +use bitwarden_exporters::parse_cxf; +use libfuzzer_sys::fuzz_target; + +// SymmetricCryptoKey parsing should never panic +fuzz_target!(|data: &[u8]| { + let payload = match String::from_utf8(data.to_vec()) { + Ok(s) => s, + Err(_) => return, // Skip invalid UTF-8 data + }; + let _ = parse_cxf(payload); +}); + diff --git a/fuzz/fuzz_targets/parse_enc_buffer.rs b/fuzz/fuzz_targets/parse_enc_buffer.rs new file mode 100644 index 000000000..4fa6aca40 --- /dev/null +++ b/fuzz/fuzz_targets/parse_enc_buffer.rs @@ -0,0 +1,9 @@ +#![no_main] + +use bitwarden_crypto::EncString; +use libfuzzer_sys::fuzz_target; + +// EncBuffer parsing should never panic +fuzz_target!(|data: &[u8]| { + let _ = EncString::from_buffer(data); +}); diff --git a/fuzz/fuzz_targets/parse_enc_string.rs b/fuzz/fuzz_targets/parse_enc_string.rs new file mode 100644 index 000000000..3e6e2a879 --- /dev/null +++ b/fuzz/fuzz_targets/parse_enc_string.rs @@ -0,0 +1,15 @@ +#![no_main] + +use std::str::FromStr; + +use bitwarden_crypto::EncString; +use libfuzzer_sys::fuzz_target; + +// EncString parsing should never panic +fuzz_target!(|data: &[u8]| { + let data_string = match std::str::from_utf8(data) { + Ok(s) => s, + Err(_) => return, + }; + let _ = EncString::from_str(data_string); +}); diff --git a/fuzz/fuzz_targets/parse_passkey.rs b/fuzz/fuzz_targets/parse_passkey.rs new file mode 100644 index 000000000..89234df03 --- /dev/null +++ b/fuzz/fuzz_targets/parse_passkey.rs @@ -0,0 +1,10 @@ +#![no_main] + +use bitwarden_fido::try_from_credential_full_view; +use bitwarden_vault::Fido2CredentialFullView; +use libfuzzer_sys::fuzz_target; + +// EncString parsing should never panic +fuzz_target!(|data: Fido2CredentialFullView| { + let _ = try_from_credential_full_view(data); +}); diff --git a/fuzz/fuzz_targets/parse_symmetric_crypto_key.rs b/fuzz/fuzz_targets/parse_symmetric_crypto_key.rs new file mode 100644 index 000000000..4eae69f08 --- /dev/null +++ b/fuzz/fuzz_targets/parse_symmetric_crypto_key.rs @@ -0,0 +1,9 @@ +#![no_main] + +use bitwarden_crypto::SymmetricCryptoKey; +use libfuzzer_sys::fuzz_target; + +// SymmetricCryptoKey parsing should never panic +fuzz_target!(|data: &[u8]| { + let _ = SymmetricCryptoKey::try_from(data.to_vec()); +}); diff --git a/fuzz/fuzz_targets/parse_unsigned_shared_key.rs b/fuzz/fuzz_targets/parse_unsigned_shared_key.rs new file mode 100644 index 000000000..baa58230c --- /dev/null +++ b/fuzz/fuzz_targets/parse_unsigned_shared_key.rs @@ -0,0 +1,15 @@ +#![no_main] + +use std::str::FromStr; + +use bitwarden_crypto::UnsignedSharedKey; +use libfuzzer_sys::fuzz_target; + +// UnsignedSharedKey parsing should never panic +fuzz_target!(|data: &[u8]| { + let data_string = match std::str::from_utf8(data) { + Ok(s) => s, + Err(_) => return, + }; + let _ = UnsignedSharedKey::from_str(data_string); +});