Skip to content

[RFC] Add parse fuzzing via cargo fuzz #345

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
83 changes: 83 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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]
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-crypto/src/aes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions crates/bitwarden-crypto/src/enc_string/symmetric.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
static ALLOC: ZeroizingAllocator<std::alloc::System> = ZeroizingAllocator(std::alloc::System);

mod aes;
pub use aes::generate_mac;
mod enc_string;
pub use enc_string::{EncString, UnsignedSharedKey};
mod error;
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-exporters/src/cxf/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
CipherType, ImportingCipher,
};

pub(crate) fn parse_cxf(payload: String) -> Result<Vec<ImportingCipher>, CxfError> {
pub fn parse_cxf(payload: String) -> Result<Vec<ImportingCipher>, CxfError> {
let account: CxfAccount = serde_json::from_str(&payload)?;

let items: Vec<ImportingCipher> = account.items.into_iter().flat_map(parse_item).collect();
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-exporters/src/cxf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion crates/bitwarden-exporters/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden-fido/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ impl TryFrom<CipherViewContainer> for Passkey {
}
}

fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result<Passkey, Fido2Error> {
pub fn try_from_credential_full_view(
value: Fido2CredentialFullView,
) -> Result<Passkey, Fido2Error> {
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)?;
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-generators/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-generators/src/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl Distribution<char> 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),
Expand Down Expand Up @@ -224,7 +224,7 @@ impl PasswordGeneratorRequest {
}

/// Implementation of the random password generator.
pub(crate) fn password(input: PasswordGeneratorRequest) -> Result<String, PasswordError> {
pub fn password(input: PasswordGeneratorRequest) -> Result<String, PasswordError> {
let options = input.validate_options()?;
Ok(password_with_rng(rand::thread_rng(), options))
}
Expand Down
1 change: 1 addition & 0 deletions crates/bitwarden-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
22 changes: 21 additions & 1 deletion crates/bitwarden-vault/src/cipher/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -151,6 +151,26 @@ pub struct Fido2CredentialFullView {
pub creation_date: DateTime<Utc>,
}

impl<'a> arbitrary::Arbitrary<'a> for Fido2CredentialFullView {
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
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::<Utc>::from_timestamp_nanos(u.arbitrary::<i64>()?),
})
}
}

// 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
Expand Down
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
66 changes: 66 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions fuzz/fuzz_targets/generate_mac.rs
Original file line number Diff line number Diff line change
@@ -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, &[], &[]);
});
Loading