Skip to content

Commit a4495a6

Browse files
authored
Merge branch 'main' into tools/pm-39238/complete-file-send-lifecycle
2 parents e4edf56 + 15ab4ca commit a4495a6

36 files changed

Lines changed: 2423 additions & 280 deletions

File tree

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ crates/bitwarden-user-crypto-management/** @bitwarden/team-key-management-dev
5959
crates/bitwarden-vault/** @bitwarden/team-vault-dev
6060

6161
# Team-owned folders in other crates (to be avoided if possible)
62-
crates/bitwarden-wasm-internal/integration-tests/tests/invite/** @bitwarden/team-admin-console-dev
62+
crates/bitwarden-wasm-internal/integration-tests/tests/organizations/** @bitwarden/team-admin-console-dev
63+
crates/bitwarden-wasm-internal/integration-tests/tests/registration/** @bitwarden/team-auth-dev
6364
crates/bitwarden-wasm-internal/integration-tests/tests/unlock/** @bitwarden/team-key-management-dev
6465
crates/bitwarden-wasm-internal/src/pure_crypto.rs @bitwarden/team-key-management-dev
6566
crates/bitwarden-core/src/key_management/** @bitwarden/team-key-management-dev @bitwarden/team-platform-dev

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-auth/Cargo.toml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,9 @@ wasm = [
2222
"dep:tsify",
2323
"dep:wasm-bindgen",
2424
"dep:wasm-bindgen-futures",
25-
] # WASM support
26-
uniffi = [
27-
"bitwarden-core/uniffi",
28-
"bitwarden-policies/uniffi",
29-
"dep:uniffi",
30-
] # Uniffi bindings
31-
secrets = ["bitwarden-core/secrets"] # Secrets Manager support
25+
]
26+
uniffi = ["bitwarden-core/uniffi", "bitwarden-policies/uniffi", "dep:uniffi"]
27+
secrets = ["bitwarden-core/secrets"]
3228

3329
# Note: dependencies must be alphabetized to pass the cargo sort check in the CI pipeline.
3430
[dependencies]
@@ -43,6 +39,7 @@ bitwarden-error = { workspace = true }
4339
bitwarden-policies = { workspace = true }
4440
bitwarden-state = { workspace = true }
4541
chrono = { workspace = true }
42+
ciborium = { workspace = true }
4643
http = { workspace = true }
4744
reqwest = { workspace = true }
4845
reqwest-middleware = { workspace = true }

crates/bitwarden-auth/src/registration/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
//! mechanisms to establish their cryptographic state and register with
44
//! the Bitwarden server
55
6+
mod open_org_invite_crypto;
67
mod post_keys_for_jit_password_registration;
78
mod post_keys_for_key_connector_registration;
89
mod post_keys_for_tde_registration;
910
mod post_keys_for_user_password_registration;
1011
mod registration_client;
1112

13+
pub use open_org_invite_crypto::{
14+
OpenOrgInvite, SealedOpenOrgInvite, SealedOpenOrgInviteData, SealedOpenOrgInviteDataError,
15+
};
1216
pub use post_keys_for_jit_password_registration::{
1317
JitMasterPasswordRegistrationRequest, JitMasterPasswordRegistrationResponse,
1418
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! FFI-facing seal/unseal. The [`RegistrationClient`] methods are thin wrappers over
2+
//! [`SealedOpenOrgInviteData::seal`] / [`SealedOpenOrgInviteData::unseal`];
3+
//! [`SealedOpenOrgInvite`] bundles both halves of the seal output as one WASM return type.
4+
5+
use bitwarden_crypto::safe::HighEntropySecret;
6+
use serde::{Deserialize, Serialize};
7+
#[cfg(feature = "wasm")]
8+
use tsify::Tsify;
9+
#[cfg(feature = "wasm")]
10+
use wasm_bindgen::prelude::*;
11+
12+
use super::{OpenOrgInvite, SealedOpenOrgInviteData};
13+
use crate::registration::registration_client::{RegistrationClient, RegistrationError};
14+
15+
/// Sealed open-organization-invite payload. Produced by
16+
/// [`RegistrationClient::seal_open_org_invite_data`] and consumed by
17+
/// [`RegistrationClient::unseal_open_org_invite_data`]. Both fields are required to unseal;
18+
/// neither half is useful on its own.
19+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
20+
#[derive(Serialize, Deserialize, Debug, Clone)]
21+
#[serde(rename_all = "camelCase")]
22+
pub struct SealedOpenOrgInvite {
23+
/// URL-safe opaque payload; place on the verification-email link.
24+
pub sealed_data: SealedOpenOrgInviteData,
25+
/// Paired secret; keep client-side (e.g. `localStorage`) and never send to the server.
26+
pub high_entropy_secret: HighEntropySecret,
27+
}
28+
29+
#[cfg_attr(feature = "wasm", wasm_bindgen)]
30+
impl RegistrationClient {
31+
/// Seals an [`OpenOrgInvite`] into a [`SealedOpenOrgInvite`]. The returned
32+
/// `sealed_data` is safe to place on the verification-email link; the returned
33+
/// `high_entropy_secret` must stay client-side.
34+
pub fn seal_open_org_invite_data(
35+
&self,
36+
input: OpenOrgInvite,
37+
) -> Result<SealedOpenOrgInvite, RegistrationError> {
38+
let (sealed_data, high_entropy_secret) = SealedOpenOrgInviteData::seal(input)?;
39+
Ok(SealedOpenOrgInvite {
40+
sealed_data,
41+
high_entropy_secret,
42+
})
43+
}
44+
45+
/// Unseals a [`SealedOpenOrgInvite`] back into an [`OpenOrgInvite`]. Returns
46+
/// [`RegistrationError::Crypto`] if the paired secret does not match the sealed payload or
47+
/// the payload has been tampered with.
48+
pub fn unseal_open_org_invite_data(
49+
&self,
50+
sealed: SealedOpenOrgInvite,
51+
) -> Result<OpenOrgInvite, RegistrationError> {
52+
sealed.sealed_data.unseal(&sealed.high_entropy_secret)
53+
}
54+
}
55+
56+
#[cfg(test)]
57+
mod tests {
58+
use bitwarden_core::Client;
59+
60+
use super::*;
61+
62+
fn sample_input() -> OpenOrgInvite {
63+
OpenOrgInvite {
64+
organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".to_string(),
65+
invite_link_code: "abcd1234efgh5678".to_string(),
66+
invite_secret: "raw-invite-secret-material-base64url".to_string(),
67+
}
68+
}
69+
70+
#[test]
71+
fn sealed_open_org_invite_json_wire_shape_is_stable() {
72+
// Locks the JSON wire: two-key camelCase object, both values as strings.
73+
let client = Client::new(None);
74+
let registration_client = RegistrationClient::new(client);
75+
let sealed = registration_client
76+
.seal_open_org_invite_data(sample_input())
77+
.expect("seal should succeed");
78+
79+
let json = serde_json::to_value(&sealed).expect("serialize");
80+
let obj = json.as_object().expect("must be a JSON object");
81+
assert_eq!(obj.len(), 2, "no extra or missing fields");
82+
assert!(
83+
obj.get("sealedData")
84+
.expect("sealedData key must be present")
85+
.is_string(),
86+
"sealedData must serialize as a JSON string"
87+
);
88+
assert!(
89+
obj.get("highEntropySecret")
90+
.expect("highEntropySecret key must be present")
91+
.is_string(),
92+
"highEntropySecret must serialize as a JSON string"
93+
);
94+
}
95+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Version 1 of the open-org-invite plaintext payload — the innermost thing sealed by the
2+
//! [`bitwarden_crypto::safe::DataEnvelope`]. Not to be confused with the sealed opaque blob
3+
//! ([`super::SealedOpenOrgInviteData`]) or the outbound JSON ([`super::SealedOpenOrgInvite`]);
4+
//! this file describes only the cleartext shape that gets CBOR-encoded and encrypted.
5+
//!
6+
//! Once this shape ships it cannot be broken: any field change means adding a new `V2` struct
7+
//! and registering both variants on the versioned enum in [`super`], so old sealed payloads
8+
//! still unseal.
9+
10+
use bitwarden_crypto::safe::SealableData;
11+
use serde::{Deserialize, Serialize};
12+
13+
#[derive(Serialize, Deserialize, Debug, PartialEq)]
14+
#[serde(rename_all = "camelCase")]
15+
pub(super) struct RegistrationOpenOrgInviteDataV1 {
16+
pub(super) organization_id: String,
17+
pub(super) invite_link_code: String,
18+
pub(super) invite_secret: String,
19+
}
20+
21+
impl SealableData for RegistrationOpenOrgInviteDataV1 {}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//! Open-organization-invite registration crossing.
2+
//!
3+
//! The app seals an invite context on registration-start submit and unseals it on the accept
4+
//! open-org-invite component after a successful registration-finish. This module owns the
5+
//! versioned plaintext payload (`data_v1`), the domain types and crypto operations
6+
//! (`open_org_invite`), their wire encoding (`serialization`), and the FFI-facing client
7+
//! methods (`client`).
8+
9+
mod client;
10+
mod data_v1;
11+
mod open_org_invite;
12+
mod serialization;
13+
14+
use bitwarden_crypto::{
15+
generate_versioned_sealable,
16+
safe::{DataEnvelopeNamespace, SealableData, SealableVersionedData},
17+
};
18+
pub use client::SealedOpenOrgInvite;
19+
use data_v1::RegistrationOpenOrgInviteDataV1;
20+
pub use open_org_invite::{OpenOrgInvite, SealedOpenOrgInviteData};
21+
use serde::{Deserialize, Serialize};
22+
pub use serialization::SealedOpenOrgInviteDataError;
23+
24+
generate_versioned_sealable!(
25+
RegistrationOpenOrgInviteData,
26+
DataEnvelopeNamespace::RegistrationOpenOrgInviteData,
27+
[RegistrationOpenOrgInviteDataV1 => "1"]
28+
);

0 commit comments

Comments
 (0)