Skip to content
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
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ crates/bitwarden-uniffi/swift/*

# Test fixtures
crates/bitwarden-exporters/resources/*
# Committed test vectors. Ignored so prettier cannot reformat them, which would change bytes that
# the Rust and TypeScript suites both read as fixed input.
test-vectors/*.json
test-vectors/**/*.json

# CI output
clippy_result.sarif
16 changes: 16 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ bitwarden-state-bridge-macro = { path = "crates/bitwarden-state-bridge-macro", v
bitwarden-sync = { path = "crates/bitwarden-sync", version = "=3.0.0" }
bitwarden-test = { path = "crates/bitwarden-test", version = "=3.0.0" }
bitwarden-test-macro = { path = "crates/bitwarden-test-macro", version = "=3.0.0" }
bitwarden-test-vector = { path = "crates/bitwarden-test-vector", version = "=3.0.0" }
bitwarden-threading = { path = "crates/bitwarden-threading", version = "=3.0.0" }
bitwarden-uniffi-error = { path = "crates/bitwarden-uniffi-error", version = "=3.0.0" }
bitwarden-unlock = { path = "crates/bitwarden-unlock", version = "=3.0.0" }
Expand Down
2 changes: 1 addition & 1 deletion crates/bitwarden-collections/src/collection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{error::CollectionsParseError, tree::TreeItem};
uuid_newtype!(pub CollectionId);

#[allow(missing_docs)]
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[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))]
Expand Down
6 changes: 3 additions & 3 deletions crates/bitwarden-core/src/key_management/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ pub enum CryptoClientError {
}

/// State used for initializing the user cryptographic state.
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[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))]
Expand All @@ -104,7 +104,7 @@ pub struct InitUserCryptoRequest {
}

/// The crypto method used to initialize the user cryptographic state.
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
Expand Down Expand Up @@ -180,7 +180,7 @@ pub enum InitUserCryptoMethod {
}

/// Auth requests supports multiple initialization methods.
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
Expand Down
6 changes: 6 additions & 0 deletions crates/bitwarden-crypto/src/signing/signing_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ impl SigningKey {
}
}

/// The key id of this signing key. The verifying key derived from it carries the same id, since
/// they are a pair.
pub fn key_id(&self) -> &KeyId {
&self.id
}

pub(super) fn cose_algorithm(&self) -> Algorithm {
match &self.inner {
RawSigningKey::Ed25519(_) => Algorithm::EdDSA,
Expand Down
6 changes: 6 additions & 0 deletions crates/bitwarden-crypto/src/signing/verifying_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ impl VerifyingKey {
}
}

/// The key id of this verifying key. This is the same id as the signing key it was derived
/// from, since they are a pair.
pub fn key_id(&self) -> &KeyId {
&self.id
}

/// Verifies the signature of the given data, for the given namespace.
/// This should never be used directly, but only through the `verify` method, to enforce
/// strong domain separation of the signatures.
Expand Down
31 changes: 31 additions & 0 deletions crates/bitwarden-test-vector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "bitwarden-test-vector"
description = """
Internal crate for the bitwarden crate. Do not use.
"""

version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
readme.workspace = true
homepage.workspace = true
repository.workspace = true
license = "GPL-3.0-only OR LicenseRef-Bitwarden-SDK"
keywords.workspace = true
publish = false

[dependencies]
bitwarden-collections = { workspace = true }
bitwarden-core = { workspace = true, features = ["internal"] }
bitwarden-crypto = { workspace = true }
bitwarden-encoding = { workspace = true }
bitwarden-send = { workspace = true }
bitwarden-vault = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
uuid = { workspace = true }

[lints]
workspace = true
8 changes: 8 additions & 0 deletions crates/bitwarden-test-vector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# bitwarden-test-vector

<div class="warning">
This crate is a development tool and is never published or shipped. It is not part of any binding.
</div>

Generate test vectors to run tests against. We want to ensure indefinite compatibility even for
formats that are no longer generated.
36 changes: 36 additions & 0 deletions crates/bitwarden-test-vector/src/emergency_access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Emergency access relationships between two user test vectors.

use bitwarden_crypto::UnsignedSharedKey;
use bitwarden_encoding::B64;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::SchemaVersioned;

/// A committed emergency access test vector: one grantor trusting one grantee with takeover.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EmergencyAccessV1Vector {
/// The on-disk schema version.
pub schema_version: u32,
/// Stable slug identifying this vector, matching its file name.
pub name: String,
/// What this vector is for, in prose.
pub description: String,
/// The emergency access record's id.
pub id: Uuid,
/// The `name` of the user vector that granted access.
pub grantor_vector: String,
/// The `name` of the user vector that received access.
pub grantee_vector: String,
/// The grantee's public key, SPKI DER.
pub grantee_public_key: B64,
/// The grantor's user key, sealed to the grantee's public key.
pub grantor_user_key_sealed_to_grantee: UnsignedSharedKey,
}

impl SchemaVersioned for EmergencyAccessV1Vector {
fn schema_version(&self) -> u32 {
self.schema_version
}
}
136 changes: 136 additions & 0 deletions crates/bitwarden-test-vector/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#![doc = include_str!("../README.md")]

use std::path::{Path, PathBuf};

mod emergency_access;
mod organization;
mod user;
mod vault;

pub use emergency_access::EmergencyAccessV1Vector;
pub use organization::{OrganizationV1MemberVector, OrganizationV1Vector};
pub use user::{AccountVector, RawCryptographicStateVector, TestVector};
pub use vault::{
AttachmentKeysVector, AttachmentVersion, CipherKeysVector, CipherVectorItem, VaultVector,
VectorItem,
};

/// The schema version the current code understands. Bumped whenever the on-disk shape changes in a
/// way that is not backward compatible, which also requires regenerating every vector.
pub const SCHEMA_VERSION: u32 = 1;

/// Errors that can occur while loading or validating a test vector.
#[derive(Debug, thiserror::Error)]
pub enum TestVectorError {
/// A vector file could not be read.
#[error("Failed to read test vector at {path}: {source}")]
Io {
/// The path that could not be read.
path: PathBuf,
/// The underlying IO error.
source: std::io::Error,
},
/// A vector file could not be parsed.
#[error("Failed to parse test vector at {path}: {source}")]
Parse {
/// The path that could not be parsed.
path: PathBuf,
/// The underlying deserialization error.
source: serde_json::Error,
},
/// A vector declares a schema version this code does not understand.
#[error("Test vector at {path} has schema version {found}, expected {SCHEMA_VERSION}")]
SchemaVersion {
/// The path of the offending vector.
path: PathBuf,
/// The version the file declares.
found: u32,
},
/// The decrypted state did not match what the vector recorded.
#[error("Validation failed for {field}: {message}")]
Validation {
/// The field that did not match.
field: &'static str,
/// What was wrong.
message: String,
},
/// A cryptographic operation failed while validating.
#[error(transparent)]
Crypto(#[from] bitwarden_crypto::CryptoError),
}

/// The `test-vectors` directory at the repository root.
///
/// Resolved from this crate's own manifest directory at compile time, so it is correct no matter
/// which crate calls it.
pub fn vectors_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("..")
.join("test-vectors")
}

/// Loads every user vector in `test-vectors/users`, sorted by file name so iteration order is
/// stable.
pub fn load_users() -> Result<Vec<TestVector>, TestVectorError> {
load_dir(&vectors_dir().join("users"))
}

/// Loads every organization vector in `test-vectors/organizations`, sorted by file name.
pub fn load_organizations() -> Result<Vec<OrganizationV1Vector>, TestVectorError> {
load_dir(&vectors_dir().join("organizations"))
}

/// Loads every emergency access vector in `test-vectors/emergency-access`, sorted by file name.
pub fn load_emergency_access() -> Result<Vec<EmergencyAccessV1Vector>, TestVectorError> {
load_dir(&vectors_dir().join("emergency-access"))
}

/// Reads and parses a single vector file.
pub fn load_file<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T, TestVectorError> {
let contents = std::fs::read_to_string(path).map_err(|source| TestVectorError::Io {
path: path.to_owned(),
source,
})?;
serde_json::from_str(&contents).map_err(|source| TestVectorError::Parse {
path: path.to_owned(),
source,
})
}

/// Reads every `.json` file in `dir`, in sorted file-name order.
fn load_dir<T: serde::de::DeserializeOwned + SchemaVersioned>(
dir: &Path,
) -> Result<Vec<T>, TestVectorError> {
let mut paths: Vec<PathBuf> = std::fs::read_dir(dir)
.map_err(|source| TestVectorError::Io {
path: dir.to_owned(),
source,
})?
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
.collect();
paths.sort();

paths
.into_iter()
.map(|path| {
let vector: T = load_file(&path)?;
if vector.schema_version() != SCHEMA_VERSION {
return Err(TestVectorError::SchemaVersion {
path,
found: vector.schema_version(),
});
}
Ok(vector)
})
.collect()
}

/// Lets the loaders reject a vector whose schema version they do not understand, before a caller
/// can act on partially-understood data.
pub trait SchemaVersioned {
/// The schema version this vector declares.
fn schema_version(&self) -> u32;
}
59 changes: 59 additions & 0 deletions crates/bitwarden-test-vector/src/organization.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! The organization half of the test vectors: the organization key, its account-recovery key pair,
//! and which user vectors are members.

use bitwarden_core::OrganizationId;
use bitwarden_crypto::{EncString, UnsignedSharedKey};
use bitwarden_encoding::B64;
use serde::{Deserialize, Serialize};

use crate::{SchemaVersioned, VaultVector};

/// A committed organization test vector.
/// [`crate::AccountVector::organization_keys`].
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct OrganizationV1Vector {
/// The on-disk schema version.
pub schema_version: u32,
/// Stable slug identifying this vector, matching its file name.
pub name: String,
/// What this vector is for, in prose.
pub description: String,
/// The organization's id.
pub organization_id: OrganizationId,
/// The organization's symmetric key, in the SDK's legacy key encoding.
pub organization_key: B64,
/// The organization key's COSE `kid`, hex encoded, or `None` if the algorithm carries none.
pub organization_key_id: Option<String>,
/// The organization's public key, SPKI DER. This is what
/// `GET /organizations/{id}/public-key` returns, and what members enroll their
/// account-recovery key against.
pub public_key: B64,
/// The organization's private key, wrapped by the organization key.
pub wrapped_private_key: EncString,
/// The organization's members.
pub members: Vec<OrganizationV1MemberVector>,
/// The organization's ciphers, encrypted and decrypted.
#[serde(default)]
pub vault: VaultVector,
}

impl SchemaVersioned for OrganizationV1Vector {
fn schema_version(&self) -> u32 {
self.schema_version
}
}

/// One member of an organization test vector.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct OrganizationV1MemberVector {
/// The `name` of the user vector this member corresponds to.
pub user_vector: String,
/// The organization key, sealed to this member's public key.
pub organization_key_sealed_to_member: UnsignedSharedKey,
/// This member's user key sealed to the organization's public key, i.e. the account-recovery
/// (admin password reset) enrollment. `None` when the member is not enrolled.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_recovery_key: Option<UnsignedSharedKey>,
}
Loading
Loading