From 171067001b2da35407a906076864eba930bfe0be Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 1 Jun 2026 16:41:38 -0400 Subject: [PATCH 01/11] implement bundle generation and unsealing --- Cargo.lock | 14 ++ Cargo.toml | 1 + .../src/invite_key_bundle.rs | 20 +-- .../Cargo.toml | 31 ++++ .../README.md | 4 + .../src/invite.rs | 170 ++++++++++++++++++ .../src/lib.rs | 7 + crates/bitwarden-wasm-internal/Cargo.toml | 2 + crates/bitwarden-wasm-internal/src/lib.rs | 1 + 9 files changed, 237 insertions(+), 13 deletions(-) create mode 100644 crates/bitwarden-organization-invite-link/Cargo.toml create mode 100644 crates/bitwarden-organization-invite-link/README.md create mode 100644 crates/bitwarden-organization-invite-link/src/invite.rs create mode 100644 crates/bitwarden-organization-invite-link/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index beb3dc9ea4..da03c69bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -894,6 +894,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "bitwarden-organization-invite-link" +version = "3.0.0" +dependencies = [ + "bitwarden-crypto", + "bitwarden-error", + "bitwarden-organization-crypto", + "serde", + "thiserror 1.0.69", + "tsify", + "wasm-bindgen", +] + [[package]] name = "bitwarden-organizations" version = "3.0.0" @@ -1312,6 +1325,7 @@ dependencies = [ "bitwarden-ipc", "bitwarden-logging", "bitwarden-organization-crypto", + "bitwarden-organization-invite-link", "bitwarden-pm", "bitwarden-policies", "bitwarden-server-communication-config", diff --git a/Cargo.toml b/Cargo.toml index a35b1076fe..eb91dbb73e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ bitwarden-generators = { path = "crates/bitwarden-generators", version = "=3.0.0 bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=3.0.0" } bitwarden-logging = { path = "crates/bitwarden-logging", version = "=3.0.0" } bitwarden-organization-crypto = { path = "crates/bitwarden-organization-crypto", version = "=3.0.0" } +bitwarden-organization-invite-link = { path = "crates/bitwarden-organization-invite-link", version = "=3.0.0" } bitwarden-organizations = { path = "crates/bitwarden-organizations", version = "=3.0.0" } bitwarden-pm = { path = "crates/bitwarden-pm", version = "=3.0.0" } bitwarden-policies = { path = "crates/bitwarden-policies", version = "=3.0.0" } diff --git a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs index 03a39edfde..8717e3d9d8 100644 --- a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs +++ b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use bitwarden_crypto::{ BitwardenLegacyKeyBytes, EncString, KeySlotIds, KeyStoreContext, SymmetricCryptoKey, }; -use bitwarden_encoding::{B64, B64Url, FromStrVisitor}; +use bitwarden_encoding::{B64Url, FromStrVisitor}; use serde::{Deserialize, Serialize}; use subtle::{Choice, ConstantTimeEq}; use thiserror::Error; @@ -95,15 +95,11 @@ impl Serialize for InviteKeyData { /// but the inner type must remain private as it may be extended in the future. pub struct InviteKeyEnvelope(EncString); +/// Serializes to the Bitwarden EncString text format (e.g. `"2.iv|data|mac"`), which is the +/// format expected by the server's `EncryptedInviteKey` field validator. impl From<&InviteKeyEnvelope> for String { fn from(key_data: &InviteKeyEnvelope) -> Self { - B64::from( - key_data - .0 - .to_buffer() - .expect("`to_buffer` never fails for `EncString`"), - ) - .to_string() + key_data.0.to_string() } } @@ -111,11 +107,9 @@ impl FromStr for InviteKeyEnvelope { type Err = InviteKeyBundleError; fn from_str(s: &str) -> Result { - let data = B64::try_from(s).map_err(|_| InviteKeyBundleError::DecodingFailed)?; - Ok(InviteKeyEnvelope( - EncString::from_buffer(data.as_bytes()) - .map_err(|_| InviteKeyBundleError::DecodingFailed)?, - )) + s.parse::() + .map(InviteKeyEnvelope) + .map_err(|_| InviteKeyBundleError::DecodingFailed) } } diff --git a/crates/bitwarden-organization-invite-link/Cargo.toml b/crates/bitwarden-organization-invite-link/Cargo.toml new file mode 100644 index 0000000000..b8e5832e64 --- /dev/null +++ b/crates/bitwarden-organization-invite-link/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "bitwarden-organization-invite-link" +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-file.workspace = true +keywords.workspace = true + +[features] +default = [] +wasm = ["bitwarden-error/wasm", "dep:tsify", "dep:wasm-bindgen"] + +[dependencies] +bitwarden-crypto = { workspace = true } +bitwarden-error = { workspace = true } +bitwarden-organization-crypto = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +tsify = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } + +[lints] +workspace = true diff --git a/crates/bitwarden-organization-invite-link/README.md b/crates/bitwarden-organization-invite-link/README.md new file mode 100644 index 0000000000..db0c811d32 --- /dev/null +++ b/crates/bitwarden-organization-invite-link/README.md @@ -0,0 +1,4 @@ +# bitwarden-organization-invite-link + +Contains the cryptographic operations for the Bitwarden admin console, including organization invite +key bundle generation. diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs new file mode 100644 index 0000000000..499e17760f --- /dev/null +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -0,0 +1,170 @@ +use std::str::FromStr; + +use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey, key_slot_ids}; +use bitwarden_error::bitwarden_error; +use bitwarden_organization_crypto::{InviteKeyBundle, InviteKeyBundleError, InviteKeyEnvelope}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify::Tsify; + +/// Errors from generating an organization invite crypto bundle. +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum OrganizationInviteCryptoBundleError { + #[error("Invalid organization key")] + InvalidOrganizationKey, + #[error("Key bundle generation failed: {0}")] + BundleGenerationFailed(#[from] InviteKeyBundleError), +} + +/// The cryptographic bundle for an organization member invite. +/// +/// - `invite_key`: raw invite key encoded as base64Url. **MUST NOT be sent to the server.** +/// - `sealed_invite_key_envelope`: invite key sealed with the org key, serialized as a Bitwarden +/// EncString (`"2.iv|data|mac"`). Safe to send to the server. +#[derive(Clone, Serialize, Deserialize, Debug)] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct OrganizationInviteCryptoBundle { + /// Raw invite key as base64Url. CRITICAL: MUST NOT be sent to the server. + pub invite_key: String, + /// Invite key sealed with the organization key, as a Bitwarden EncString (`"2.iv|data|mac"`). + pub sealed_invite_key_envelope: String, +} + +/// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the provided organization key. +/// +/// Each call produces a unique, non-deterministic invite key. +/// +/// # Security +/// The `invite_key` field MUST NOT be sent to the server. +#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)] +pub fn generate_organization_invite_crypto_bundle( + org_key: Vec, +) -> Result { + let tmp_store: KeyStore = KeyStore::default(); + let mut context = tmp_store.context(); + + let org_key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(org_key)) + .map_err(|_| OrganizationInviteCryptoBundleError::InvalidOrganizationKey)?; + let org_key_slot = context.add_local_symmetric_key(org_key); + + let bundle = InviteKeyBundle::make(org_key_slot, &mut context)?; + + Ok(OrganizationInviteCryptoBundle { + invite_key: String::from(bundle.dangerous_get_raw_invite_key()), + sealed_invite_key_envelope: String::from(bundle.get_sealed_invite_key_envelope()), + }) +} + +/// Unseals a `sealedInviteKeyEnvelope` (produced by [`generate_organization_invite_crypto_bundle`]) +/// using the organization key, returning the raw invite key as a base64Url string. +/// +/// The returned invite key is safe to embed in a URL fragment for distribution to invitees. +#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)] +pub fn unseal_organization_invite_key( + org_key: Vec, + sealed_invite_key_envelope: String, +) -> Result { + let tmp_store: KeyStore = KeyStore::default(); + let mut context = tmp_store.context(); + + let org_key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(org_key)) + .map_err(|_| OrganizationInviteCryptoBundleError::InvalidOrganizationKey)?; + let org_key_slot = context.add_local_symmetric_key(org_key); + + let envelope = InviteKeyEnvelope::from_str(&sealed_invite_key_envelope) + .map_err(OrganizationInviteCryptoBundleError::BundleGenerationFailed)?; + let invite_key_data = envelope + .unseal(org_key_slot, &mut context) + .map_err(OrganizationInviteCryptoBundleError::BundleGenerationFailed)?; + + Ok(String::from(&invite_key_data)) +} + +key_slot_ids! { + #[symmetric] + enum LocalSymmetricKeySlotId { + #[local] + Local(LocalId), + } + + #[private] + enum LocalPrivateKeySlotId { + #[local] + Local(LocalId), + } + + #[signing] + enum LocalSigningKeySlotId { + #[local] + Local(LocalId), + } + + LocalKeySlotIds => LocalSymmetricKeySlotId, LocalPrivateKeySlotId, LocalSigningKeySlotId; +} + +#[cfg(test)] +mod tests { + use bitwarden_crypto::SymmetricCryptoKey; + + use super::*; + + fn make_org_key() -> Vec { + SymmetricCryptoKey::make_aes256_cbc_hmac_key() + .to_encoded() + .to_vec() + } + + #[test] + fn test_bundle_returns_valid_non_empty_strings() { + let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); + assert!(!bundle.invite_key.is_empty()); + assert!(!bundle.sealed_invite_key_envelope.is_empty()); + } + + #[test] + fn test_envelope_unseals_to_raw_invite_key() { + let org_key_bytes = make_org_key(); + let bundle = generate_organization_invite_crypto_bundle(org_key_bytes.clone()).unwrap(); + + let unsealed = unseal_organization_invite_key( + org_key_bytes, + bundle.sealed_invite_key_envelope.clone(), + ) + .unwrap(); + + assert_eq!(bundle.invite_key, unsealed); + } + + #[test] + fn test_two_calls_produce_different_invite_keys() { + let org_key = make_org_key(); + let bundle1 = generate_organization_invite_crypto_bundle(org_key.clone()).unwrap(); + let bundle2 = generate_organization_invite_crypto_bundle(org_key).unwrap(); + assert_ne!(bundle1.invite_key, bundle2.invite_key); + } + + #[test] + fn test_sealed_invite_key_envelope_is_encstring_text_format() { + // The server validates EncryptedInviteKey as a Bitwarden EncString text + // format (e.g. "2.iv|data|mac"). + let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); + let envelope = &bundle.sealed_invite_key_envelope; + assert!( + envelope.parse::().is_ok(), + "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope}" + ); + } + + #[test] + fn test_invalid_org_key_returns_error() { + let result = generate_organization_invite_crypto_bundle(vec![0u8; 4]); + assert!(matches!( + result, + Err(OrganizationInviteCryptoBundleError::InvalidOrganizationKey) + )); + } +} diff --git a/crates/bitwarden-organization-invite-link/src/lib.rs b/crates/bitwarden-organization-invite-link/src/lib.rs new file mode 100644 index 0000000000..3b422e766e --- /dev/null +++ b/crates/bitwarden-organization-invite-link/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] + +mod invite; +pub use invite::{ + OrganizationInviteCryptoBundle, OrganizationInviteCryptoBundleError, + generate_organization_invite_crypto_bundle, unseal_organization_invite_key, +}; diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index ed26b038cf..9f20dc2397 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -15,6 +15,7 @@ keywords.workspace = true [package.metadata.cargo-udeps.ignore] normal = [ "bitwarden-organization-crypto", + "bitwarden-organization-invite-link", ] # Only used to enable wasm-bindgen, not imported directly [lib] @@ -44,6 +45,7 @@ bitwarden-logging = { workspace = true, features = ["wasm"] } # ensure wasm-bindgen is generated for the `bitwarden-organization-crypto` crate # See [package.metadata.cargo-udeps.ignore] above for more. bitwarden-organization-crypto = { workspace = true, features = ["wasm"] } +bitwarden-organization-invite-link = { workspace = true, features = ["wasm"] } bitwarden-pm = { workspace = true, features = ["wasm"] } bitwarden-policies = { workspace = true, features = ["wasm"] } bitwarden-server-communication-config = { workspace = true, features = ["wasm"] } diff --git a/crates/bitwarden-wasm-internal/src/lib.rs b/crates/bitwarden-wasm-internal/src/lib.rs index 968c0b6821..bae57f17d6 100644 --- a/crates/bitwarden-wasm-internal/src/lib.rs +++ b/crates/bitwarden-wasm-internal/src/lib.rs @@ -9,6 +9,7 @@ mod pure_crypto; mod ssh; pub use bitwarden_ipc::wasm::*; +pub use bitwarden_organization_invite_link::*; pub use bitwarden_server_communication_config::wasm::*; pub use bitwarden_shared_unlock::wasm::*; pub use client::PasswordManagerClient; From 2efd3938cd7e0f7e4611e9f5e7fceda823c3b420 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 2 Jun 2026 12:05:30 -0400 Subject: [PATCH 02/11] redact key in debug, clean up --- .../src/invite.rs | 37 +++++++++++++++++-- crates/bitwarden-wasm-internal/Cargo.toml | 1 - 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs index 499e17760f..328e8dc611 100644 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -17,6 +17,10 @@ pub enum OrganizationInviteCryptoBundleError { InvalidOrganizationKey, #[error("Key bundle generation failed: {0}")] BundleGenerationFailed(#[from] InviteKeyBundleError), + #[error("Invalid sealed invite key envelope: {0}")] + InvalidSealedEnvelope(InviteKeyBundleError), + #[error("Failed to unseal invite key: {0}")] + UnsealingFailed(InviteKeyBundleError), } /// The cryptographic bundle for an organization member invite. @@ -24,7 +28,7 @@ pub enum OrganizationInviteCryptoBundleError { /// - `invite_key`: raw invite key encoded as base64Url. **MUST NOT be sent to the server.** /// - `sealed_invite_key_envelope`: invite key sealed with the org key, serialized as a Bitwarden /// EncString (`"2.iv|data|mac"`). Safe to send to the server. -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] #[serde(rename_all = "camelCase")] pub struct OrganizationInviteCryptoBundle { @@ -34,6 +38,18 @@ pub struct OrganizationInviteCryptoBundle { pub sealed_invite_key_envelope: String, } +impl std::fmt::Debug for OrganizationInviteCryptoBundle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OrganizationInviteCryptoBundle") + .field("invite_key", &"") + .field( + "sealed_invite_key_envelope", + &self.sealed_invite_key_envelope, + ) + .finish() + } +} + /// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the provided organization key. /// /// Each call produces a unique, non-deterministic invite key. @@ -76,10 +92,10 @@ pub fn unseal_organization_invite_key( let org_key_slot = context.add_local_symmetric_key(org_key); let envelope = InviteKeyEnvelope::from_str(&sealed_invite_key_envelope) - .map_err(OrganizationInviteCryptoBundleError::BundleGenerationFailed)?; + .map_err(OrganizationInviteCryptoBundleError::InvalidSealedEnvelope)?; let invite_key_data = envelope .unseal(org_key_slot, &mut context) - .map_err(OrganizationInviteCryptoBundleError::BundleGenerationFailed)?; + .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)?; Ok(String::from(&invite_key_data)) } @@ -167,4 +183,19 @@ mod tests { Err(OrganizationInviteCryptoBundleError::InvalidOrganizationKey) )); } + + #[test] + fn test_unseal_with_wrong_org_key_fails() { + let org_key_1 = make_org_key(); + let org_key_2 = make_org_key(); + + let bundle = generate_organization_invite_crypto_bundle(org_key_1).unwrap(); + + let result = unseal_organization_invite_key(org_key_2, bundle.sealed_invite_key_envelope); + + assert!( + result.is_err(), + "Unsealing with the wrong org key must fail" + ); + } } diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 9f20dc2397..9a44c6f7e8 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -15,7 +15,6 @@ keywords.workspace = true [package.metadata.cargo-udeps.ignore] normal = [ "bitwarden-organization-crypto", - "bitwarden-organization-invite-link", ] # Only used to enable wasm-bindgen, not imported directly [lib] From ffd31979669ab64d4620bccba1ef98d6e4f3b258 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 2 Jun 2026 15:50:22 -0400 Subject: [PATCH 03/11] update codeowners and docs --- .github/CODEOWNERS | 1 + .../examples/make_invite_key_bundle.rs | 4 ++-- crates/bitwarden-organization-crypto/src/wasm.rs | 9 +++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 06ade795fa..6125fc304d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -41,6 +41,7 @@ crates/bitwarden-crypto/** @bitwarden/team-key-management-dev crates/bitwarden-exporters/** @bitwarden/team-tools-dev @bitwarden/team-platform-dev crates/bitwarden-generators/** @bitwarden/team-tools-dev @bitwarden/team-platform-dev crates/bitwarden-organization-crypto/** @bitwarden/team-key-management-dev +crates/bitwarden-organization-invite-link/** @bitwarden/team-admin-console-dev crates/bitwarden-organizations/** @bitwarden/team-admin-console-dev crates/bitwarden-policies/** @bitwarden/team-admin-console-dev crates/bitwarden-send/** @bitwarden/team-tools-dev @bitwarden/team-platform-dev diff --git a/crates/bitwarden-organization-crypto/examples/make_invite_key_bundle.rs b/crates/bitwarden-organization-crypto/examples/make_invite_key_bundle.rs index dcb2f91a59..2e2aa3ef45 100644 --- a/crates/bitwarden-organization-crypto/examples/make_invite_key_bundle.rs +++ b/crates/bitwarden-organization-crypto/examples/make_invite_key_bundle.rs @@ -29,8 +29,8 @@ fn main() { let key: &InviteKeyData = bundle.dangerous_get_raw_invite_key(); // 3. The second part is `InviteKeyEnvelope`. This is the invite - // key sealed (a.k.a. sealed) by the org key. `InviteKeyEnvelope` - // automatically serializes to `base64` when using serde, + // key sealed by the org key. `InviteKeyEnvelope` serializes to the + // Bitwarden EncString text format (`"2.iv|data|mac"`) when using serde, // `String::from(&inviteKeyEnvelope)`, or wasm abi serialization. let organization_wrapped_invitation_key: &InviteKeyEnvelope = bundle.get_sealed_invite_key_envelope(); diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index 8a639fa513..087a4c4e39 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -1,9 +1,10 @@ //! The wasm module holds serialization/encoding needed wasm bindings for //! any types related to InviteKeyEnvelope. This means base64url for the -//! InviteKeyData type, and base64 for the InviteKey type. In order to minimize -//! complexity, the actual B64/B64Url encoding/decoding are limited to the -//! `From` and `FromStr` implementations. All other serialization -//! goes through String to simplify maintenance. +//! InviteKeyData type, and Bitwarden EncString text format (`"2.iv|data|mac"`) +//! for the InviteKeyEnvelope type. In order to minimize complexity, the actual +//! encoding/decoding is limited to the `From` and `FromStr` +//! implementations. All other serialization goes through String to simplify +//! maintenance. use std::str::FromStr; use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi}; From a77ee1a90a5e414b4969e7b5ac06ee2a2bd18b44 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 3 Jun 2026 16:50:21 -0400 Subject: [PATCH 04/11] use specific types, fix bindings to use said types --- .../src/invite_key_bundle.rs | 12 +- .../bitwarden-organization-crypto/src/wasm.rs | 15 +- .../Cargo.toml | 8 +- .../src/invite.rs | 147 ++++++++++-------- 4 files changed, 107 insertions(+), 75 deletions(-) diff --git a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs index 8717e3d9d8..c66ad61d88 100644 --- a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs +++ b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs @@ -93,9 +93,10 @@ impl Serialize for InviteKeyData { /// Struct for holding the wrapped invite key data. Currently supports encstring /// but the inner type must remain private as it may be extended in the future. +#[derive(Clone)] pub struct InviteKeyEnvelope(EncString); -/// Serializes to the Bitwarden EncString text format (e.g. `"2.iv|data|mac"`), which is the +/// Serializes to the Bitwarden EncString text format, which is the /// format expected by the server's `EncryptedInviteKey` field validator. impl From<&InviteKeyEnvelope> for String { fn from(key_data: &InviteKeyEnvelope) -> Self { @@ -268,7 +269,14 @@ mod tests { .unwrap_symmetric_key(TestSymmKey::Organization, &key.sealed_key_envelope.0) .unwrap(); - ctx.assert_symmetric_keys_equal(raw_key_id, unsealed_key); + #[allow(deprecated)] + let raw_key = ctx.dangerous_get_symmetric_key(raw_key_id).unwrap().clone(); + #[allow(deprecated)] + let unsealed = ctx + .dangerous_get_symmetric_key(unsealed_key) + .unwrap() + .clone(); + assert_eq!(raw_key, unsealed); } #[test] diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index 087a4c4e39..0bd774f9d4 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -12,10 +12,18 @@ use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi}; use crate::{InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope}; #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_CUSTOM_TYPES: &'static str = r#" +const TS_INVITE_KEY_DATA: &'static str = r#" export type InviteKeyData = Tagged; "#; +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_INVITE_KEY_ENVELOPE: &'static str = r#" +export type InviteKeyEnvelope = Tagged; +"#; + +#[wasm_bindgen::prelude::wasm_bindgen] +pub struct OrganizationCryptoWasm; + impl wasm_bindgen::describe::WasmDescribe for InviteKeyData { fn describe() { ::describe(); @@ -57,11 +65,6 @@ impl TryFrom for InviteKeyData { } } -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_CUSTOM_TYPES: &'static str = r#" -export type InviteKeyEnvelope = Tagged; -"#; - impl wasm_bindgen::describe::WasmDescribe for InviteKeyEnvelope { fn describe() { ::describe(); diff --git a/crates/bitwarden-organization-invite-link/Cargo.toml b/crates/bitwarden-organization-invite-link/Cargo.toml index b8e5832e64..aa964a4bc2 100644 --- a/crates/bitwarden-organization-invite-link/Cargo.toml +++ b/crates/bitwarden-organization-invite-link/Cargo.toml @@ -16,7 +16,13 @@ keywords.workspace = true [features] default = [] -wasm = ["bitwarden-error/wasm", "dep:tsify", "dep:wasm-bindgen"] +wasm = [ + "bitwarden-error/wasm", + "bitwarden-crypto/wasm", + "bitwarden-organization-crypto/wasm", + "dep:tsify", + "dep:wasm-bindgen", +] [dependencies] bitwarden-crypto = { workspace = true } diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs index 328e8dc611..9bb6cefaa5 100644 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -1,77 +1,89 @@ -use std::str::FromStr; - -use bitwarden_crypto::{BitwardenLegacyKeyBytes, KeyStore, SymmetricCryptoKey, key_slot_ids}; +use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, key_slot_ids}; use bitwarden_error::bitwarden_error; -use bitwarden_organization_crypto::{InviteKeyBundle, InviteKeyBundleError, InviteKeyEnvelope}; +use bitwarden_organization_crypto::{ + InviteKeyBundle, InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope, +}; use serde::{Deserialize, Serialize}; use thiserror::Error; #[cfg(feature = "wasm")] use tsify::Tsify; +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_FUNCTIONS: &'static str = r#" +/** + * Generates a new organization invite crypto bundle sealed with the provided organization key. + * The `inviteKey` field MUST NOT be sent to the server. + */ +export function generate_organization_invite_crypto_bundle(org_key: SymmetricKey): OrganizationInviteCryptoBundle; + +/** + * Unseals a sealed invite key envelope using the organization key, + * returning the raw invite key as a base64Url string. + */ +export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): string; +"#; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen] +pub struct OrganizationInviteLinkWasm; + /// Errors from generating an organization invite crypto bundle. -#[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, Error)] pub enum OrganizationInviteCryptoBundleError { - #[error("Invalid organization key")] - InvalidOrganizationKey, #[error("Key bundle generation failed: {0}")] + /// Bundle Generation failed BundleGenerationFailed(#[from] InviteKeyBundleError), #[error("Invalid sealed invite key envelope: {0}")] + /// Invalid Envelope InvalidSealedEnvelope(InviteKeyBundleError), #[error("Failed to unseal invite key: {0}")] + /// Unsealing Envelope failed UnsealingFailed(InviteKeyBundleError), } /// The cryptographic bundle for an organization member invite. /// /// - `invite_key`: raw invite key encoded as base64Url. **MUST NOT be sent to the server.** -/// - `sealed_invite_key_envelope`: invite key sealed with the org key, serialized as a Bitwarden -/// EncString (`"2.iv|data|mac"`). Safe to send to the server. +/// - `sealed_invite_key_envelope`: invite key sealed with the org key, serialized as an EncString. +/// Safe to send to the server. #[derive(Clone, Serialize, Deserialize)] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] #[serde(rename_all = "camelCase")] pub struct OrganizationInviteCryptoBundle { - /// Raw invite key as base64Url. CRITICAL: MUST NOT be sent to the server. - pub invite_key: String, - /// Invite key sealed with the organization key, as a Bitwarden EncString (`"2.iv|data|mac"`). - pub sealed_invite_key_envelope: String, -} - -impl std::fmt::Debug for OrganizationInviteCryptoBundle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("OrganizationInviteCryptoBundle") - .field("invite_key", &"") - .field( - "sealed_invite_key_envelope", - &self.sealed_invite_key_envelope, - ) - .finish() - } + /// Key data containing the raw invite key bytes as base64Url. CRITICAL: MUST NOT be sent to + /// the server. + #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyData"))] + pub invite_key: InviteKeyData, + /// Invite key sealed with the organization key + #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyEnvelope"))] + pub sealed_invite_key_envelope: InviteKeyEnvelope, } /// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the provided organization key. /// -/// Each call produces a unique, non-deterministic invite key. +/// Each call produces a unique key sampled from a secure cryptographic source. /// /// # Security /// The `invite_key` field MUST NOT be sent to the server. -#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)] +#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen(skip_typescript))] pub fn generate_organization_invite_crypto_bundle( - org_key: Vec, + org_key: SymmetricCryptoKey, ) -> Result { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); - let org_key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(org_key)) - .map_err(|_| OrganizationInviteCryptoBundleError::InvalidOrganizationKey)?; let org_key_slot = context.add_local_symmetric_key(org_key); let bundle = InviteKeyBundle::make(org_key_slot, &mut context)?; + let invite_key = bundle.dangerous_get_raw_invite_key().clone(); + let sealed_invite_key_envelope = bundle.get_sealed_invite_key_envelope().clone(); + Ok(OrganizationInviteCryptoBundle { - invite_key: String::from(bundle.dangerous_get_raw_invite_key()), - sealed_invite_key_envelope: String::from(bundle.get_sealed_invite_key_envelope()), + invite_key, + sealed_invite_key_envelope, }) } @@ -79,21 +91,17 @@ pub fn generate_organization_invite_crypto_bundle( /// using the organization key, returning the raw invite key as a base64Url string. /// /// The returned invite key is safe to embed in a URL fragment for distribution to invitees. -#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen)] +#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen(skip_typescript))] pub fn unseal_organization_invite_key( - org_key: Vec, - sealed_invite_key_envelope: String, + org_key: SymmetricCryptoKey, + sealed_invite_key_envelope: InviteKeyEnvelope, ) -> Result { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); - let org_key = SymmetricCryptoKey::try_from(&BitwardenLegacyKeyBytes::from(org_key)) - .map_err(|_| OrganizationInviteCryptoBundleError::InvalidOrganizationKey)?; let org_key_slot = context.add_local_symmetric_key(org_key); - let envelope = InviteKeyEnvelope::from_str(&sealed_invite_key_envelope) - .map_err(OrganizationInviteCryptoBundleError::InvalidSealedEnvelope)?; - let invite_key_data = envelope + let invite_key_data = sealed_invite_key_envelope .unseal(org_key_slot, &mut context) .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)?; @@ -128,39 +136,55 @@ mod tests { use super::*; - fn make_org_key() -> Vec { + fn make_org_key() -> SymmetricCryptoKey { SymmetricCryptoKey::make_aes256_cbc_hmac_key() - .to_encoded() - .to_vec() } #[test] - fn test_bundle_returns_valid_non_empty_strings() { + fn test_bundle_returns_valid_invite_key_and_envelope() { let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); - assert!(!bundle.invite_key.is_empty()); - assert!(!bundle.sealed_invite_key_envelope.is_empty()); + // InviteKeyData serializes as a non-empty base64Url string + assert!(!String::from(&bundle.invite_key).is_empty()); + // InviteKeyEnvelope serializes as a non-empty EncString + assert!(!String::from(&bundle.sealed_invite_key_envelope).is_empty()); } #[test] fn test_envelope_unseals_to_raw_invite_key() { - let org_key_bytes = make_org_key(); - let bundle = generate_organization_invite_crypto_bundle(org_key_bytes.clone()).unwrap(); + let org_key_bytes = make_org_key().to_encoded().to_vec(); + let org_key = SymmetricCryptoKey::try_from( + &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key_bytes.clone()), + ) + .unwrap(); + let org_key_for_unseal = SymmetricCryptoKey::try_from( + &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key_bytes), + ) + .unwrap(); + + let bundle = generate_organization_invite_crypto_bundle(org_key).unwrap(); let unsealed = unseal_organization_invite_key( - org_key_bytes, + org_key_for_unseal, bundle.sealed_invite_key_envelope.clone(), ) .unwrap(); - assert_eq!(bundle.invite_key, unsealed); + assert_eq!(String::from(&bundle.invite_key), unsealed); } #[test] fn test_two_calls_produce_different_invite_keys() { - let org_key = make_org_key(); - let bundle1 = generate_organization_invite_crypto_bundle(org_key.clone()).unwrap(); - let bundle2 = generate_organization_invite_crypto_bundle(org_key).unwrap(); - assert_ne!(bundle1.invite_key, bundle2.invite_key); + let org_key1 = make_org_key(); + let org_key2 = SymmetricCryptoKey::try_from( + &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key1.to_encoded().to_vec()), + ) + .unwrap(); + let bundle1 = generate_organization_invite_crypto_bundle(org_key1).unwrap(); + let bundle2 = generate_organization_invite_crypto_bundle(org_key2).unwrap(); + assert_ne!( + String::from(&bundle1.invite_key), + String::from(&bundle2.invite_key) + ); } #[test] @@ -168,22 +192,13 @@ mod tests { // The server validates EncryptedInviteKey as a Bitwarden EncString text // format (e.g. "2.iv|data|mac"). let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); - let envelope = &bundle.sealed_invite_key_envelope; + let envelope_str = String::from(&bundle.sealed_invite_key_envelope); assert!( - envelope.parse::().is_ok(), - "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope}" + envelope_str.parse::().is_ok(), + "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope_str}" ); } - #[test] - fn test_invalid_org_key_returns_error() { - let result = generate_organization_invite_crypto_bundle(vec![0u8; 4]); - assert!(matches!( - result, - Err(OrganizationInviteCryptoBundleError::InvalidOrganizationKey) - )); - } - #[test] fn test_unseal_with_wrong_org_key_fails() { let org_key_1 = make_org_key(); From f2a179d16f54607cdb513be6251a80b307d8ec4c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 4 Jun 2026 10:02:20 -0400 Subject: [PATCH 05/11] add doc comment --- crates/bitwarden-organization-invite-link/src/invite.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs index 9bb6cefaa5..cc6c65ddd9 100644 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -24,6 +24,10 @@ export function generate_organization_invite_crypto_bundle(org_key: SymmetricKey export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): string; "#; +/// WASM bindings for organization invite link cryptographic operations. +/// +/// Exposes [`generate_organization_invite_crypto_bundle`] and +/// [`unseal_organization_invite_key`] to JavaScript/TypeScript consumers. #[cfg(feature = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen] pub struct OrganizationInviteLinkWasm; From 9b31c36f0b37292d491ac45f01d8f771865529a5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 4 Jun 2026 11:40:26 -0400 Subject: [PATCH 06/11] use tagged string type instead of String --- .../src/invite.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs index cc6c65ddd9..cd67301391 100644 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -21,7 +21,7 @@ export function generate_organization_invite_crypto_bundle(org_key: SymmetricKey * Unseals a sealed invite key envelope using the organization key, * returning the raw invite key as a base64Url string. */ -export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): string; +export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): InviteKeyData; "#; /// WASM bindings for organization invite link cryptographic operations. @@ -39,9 +39,6 @@ pub enum OrganizationInviteCryptoBundleError { #[error("Key bundle generation failed: {0}")] /// Bundle Generation failed BundleGenerationFailed(#[from] InviteKeyBundleError), - #[error("Invalid sealed invite key envelope: {0}")] - /// Invalid Envelope - InvalidSealedEnvelope(InviteKeyBundleError), #[error("Failed to unseal invite key: {0}")] /// Unsealing Envelope failed UnsealingFailed(InviteKeyBundleError), @@ -99,17 +96,15 @@ pub fn generate_organization_invite_crypto_bundle( pub fn unseal_organization_invite_key( org_key: SymmetricCryptoKey, sealed_invite_key_envelope: InviteKeyEnvelope, -) -> Result { +) -> Result { let tmp_store: KeyStore = KeyStore::default(); let mut context = tmp_store.context(); let org_key_slot = context.add_local_symmetric_key(org_key); - let invite_key_data = sealed_invite_key_envelope + sealed_invite_key_envelope .unseal(org_key_slot, &mut context) - .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)?; - - Ok(String::from(&invite_key_data)) + .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed) } key_slot_ids! { @@ -173,7 +168,7 @@ mod tests { ) .unwrap(); - assert_eq!(String::from(&bundle.invite_key), unsealed); + assert_eq!(bundle.invite_key, unsealed); } #[test] From cbfff3d79e625527c8267761a3c7056296d33ede Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 4 Jun 2026 12:10:14 -0400 Subject: [PATCH 07/11] update docs --- crates/bitwarden-organization-crypto/src/wasm.rs | 1 + crates/bitwarden-organization-invite-link/src/invite.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index 0bd774f9d4..e419f23f54 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -21,6 +21,7 @@ const TS_INVITE_KEY_ENVELOPE: &'static str = r#" export type InviteKeyEnvelope = Tagged; "#; +/// WASM bindings for organization cryptography operations. #[wasm_bindgen::prelude::wasm_bindgen] pub struct OrganizationCryptoWasm; diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs index cd67301391..a565748f1a 100644 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ b/crates/bitwarden-organization-invite-link/src/invite.rs @@ -19,7 +19,7 @@ export function generate_organization_invite_crypto_bundle(org_key: SymmetricKey /** * Unseals a sealed invite key envelope using the organization key, - * returning the raw invite key as a base64Url string. + * returning the raw invite key as an InviteKeyData. */ export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): InviteKeyData; "#; @@ -89,7 +89,7 @@ pub fn generate_organization_invite_crypto_bundle( } /// Unseals a `sealedInviteKeyEnvelope` (produced by [`generate_organization_invite_crypto_bundle`]) -/// using the organization key, returning the raw invite key as a base64Url string. +/// using the organization key, returning the raw invite key as [`InviteKeyData`]. /// /// The returned invite key is safe to embed in a URL fragment for distribution to invitees. #[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen(skip_typescript))] From 09b863150d5c036bedaf5d559b1f4556c56e822d Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 8 Jun 2026 17:06:06 -0400 Subject: [PATCH 08/11] reafactor to client pattern for invite link --- Cargo.lock | 2 + .../bitwarden-organization-crypto/src/wasm.rs | 4 - .../Cargo.toml | 4 +- .../src/invite.rs | 215 ------------------ .../src/invite_link_client.rs | 214 +++++++++++++++++ .../src/lib.rs | 8 +- crates/bitwarden-pm/Cargo.toml | 2 + crates/bitwarden-pm/src/lib.rs | 7 + crates/bitwarden-wasm-internal/src/client.rs | 5 + 9 files changed, 237 insertions(+), 224 deletions(-) delete mode 100644 crates/bitwarden-organization-invite-link/src/invite.rs create mode 100644 crates/bitwarden-organization-invite-link/src/invite_link_client.rs diff --git a/Cargo.lock b/Cargo.lock index da03c69bb6..219d7a2a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -898,6 +898,7 @@ dependencies = [ name = "bitwarden-organization-invite-link" version = "3.0.0" dependencies = [ + "bitwarden-core", "bitwarden-crypto", "bitwarden-error", "bitwarden-organization-crypto", @@ -932,6 +933,7 @@ dependencies = [ "bitwarden-exporters", "bitwarden-fido", "bitwarden-generators", + "bitwarden-organization-invite-link", "bitwarden-policies", "bitwarden-send", "bitwarden-server-communication-config", diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index e419f23f54..83543c29c4 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -21,10 +21,6 @@ const TS_INVITE_KEY_ENVELOPE: &'static str = r#" export type InviteKeyEnvelope = Tagged; "#; -/// WASM bindings for organization cryptography operations. -#[wasm_bindgen::prelude::wasm_bindgen] -pub struct OrganizationCryptoWasm; - impl wasm_bindgen::describe::WasmDescribe for InviteKeyData { fn describe() { ::describe(); diff --git a/crates/bitwarden-organization-invite-link/Cargo.toml b/crates/bitwarden-organization-invite-link/Cargo.toml index aa964a4bc2..5b82a9faff 100644 --- a/crates/bitwarden-organization-invite-link/Cargo.toml +++ b/crates/bitwarden-organization-invite-link/Cargo.toml @@ -17,14 +17,16 @@ keywords.workspace = true [features] default = [] wasm = [ - "bitwarden-error/wasm", + "bitwarden-core/wasm", "bitwarden-crypto/wasm", + "bitwarden-error/wasm", "bitwarden-organization-crypto/wasm", "dep:tsify", "dep:wasm-bindgen", ] [dependencies] +bitwarden-core = { workspace = true } bitwarden-crypto = { workspace = true } bitwarden-error = { workspace = true } bitwarden-organization-crypto = { workspace = true } diff --git a/crates/bitwarden-organization-invite-link/src/invite.rs b/crates/bitwarden-organization-invite-link/src/invite.rs deleted file mode 100644 index a565748f1a..0000000000 --- a/crates/bitwarden-organization-invite-link/src/invite.rs +++ /dev/null @@ -1,215 +0,0 @@ -use bitwarden_crypto::{KeyStore, SymmetricCryptoKey, key_slot_ids}; -use bitwarden_error::bitwarden_error; -use bitwarden_organization_crypto::{ - InviteKeyBundle, InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope, -}; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -#[cfg(feature = "wasm")] -use tsify::Tsify; - -#[cfg(feature = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_FUNCTIONS: &'static str = r#" -/** - * Generates a new organization invite crypto bundle sealed with the provided organization key. - * The `inviteKey` field MUST NOT be sent to the server. - */ -export function generate_organization_invite_crypto_bundle(org_key: SymmetricKey): OrganizationInviteCryptoBundle; - -/** - * Unseals a sealed invite key envelope using the organization key, - * returning the raw invite key as an InviteKeyData. - */ -export function unseal_organization_invite_key(org_key: SymmetricKey, sealed_invite_key_envelope: InviteKeyEnvelope): InviteKeyData; -"#; - -/// WASM bindings for organization invite link cryptographic operations. -/// -/// Exposes [`generate_organization_invite_crypto_bundle`] and -/// [`unseal_organization_invite_key`] to JavaScript/TypeScript consumers. -#[cfg(feature = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen] -pub struct OrganizationInviteLinkWasm; - -/// Errors from generating an organization invite crypto bundle. -#[bitwarden_error(flat)] -#[derive(Debug, Error)] -pub enum OrganizationInviteCryptoBundleError { - #[error("Key bundle generation failed: {0}")] - /// Bundle Generation failed - BundleGenerationFailed(#[from] InviteKeyBundleError), - #[error("Failed to unseal invite key: {0}")] - /// Unsealing Envelope failed - UnsealingFailed(InviteKeyBundleError), -} - -/// The cryptographic bundle for an organization member invite. -/// -/// - `invite_key`: raw invite key encoded as base64Url. **MUST NOT be sent to the server.** -/// - `sealed_invite_key_envelope`: invite key sealed with the org key, serialized as an EncString. -/// Safe to send to the server. -#[derive(Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -#[serde(rename_all = "camelCase")] -pub struct OrganizationInviteCryptoBundle { - /// Key data containing the raw invite key bytes as base64Url. CRITICAL: MUST NOT be sent to - /// the server. - #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyData"))] - pub invite_key: InviteKeyData, - /// Invite key sealed with the organization key - #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyEnvelope"))] - pub sealed_invite_key_envelope: InviteKeyEnvelope, -} - -/// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the provided organization key. -/// -/// Each call produces a unique key sampled from a secure cryptographic source. -/// -/// # Security -/// The `invite_key` field MUST NOT be sent to the server. -#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen(skip_typescript))] -pub fn generate_organization_invite_crypto_bundle( - org_key: SymmetricCryptoKey, -) -> Result { - let tmp_store: KeyStore = KeyStore::default(); - let mut context = tmp_store.context(); - - let org_key_slot = context.add_local_symmetric_key(org_key); - - let bundle = InviteKeyBundle::make(org_key_slot, &mut context)?; - - let invite_key = bundle.dangerous_get_raw_invite_key().clone(); - let sealed_invite_key_envelope = bundle.get_sealed_invite_key_envelope().clone(); - - Ok(OrganizationInviteCryptoBundle { - invite_key, - sealed_invite_key_envelope, - }) -} - -/// Unseals a `sealedInviteKeyEnvelope` (produced by [`generate_organization_invite_crypto_bundle`]) -/// using the organization key, returning the raw invite key as [`InviteKeyData`]. -/// -/// The returned invite key is safe to embed in a URL fragment for distribution to invitees. -#[cfg_attr(feature = "wasm", wasm_bindgen::prelude::wasm_bindgen(skip_typescript))] -pub fn unseal_organization_invite_key( - org_key: SymmetricCryptoKey, - sealed_invite_key_envelope: InviteKeyEnvelope, -) -> Result { - let tmp_store: KeyStore = KeyStore::default(); - let mut context = tmp_store.context(); - - let org_key_slot = context.add_local_symmetric_key(org_key); - - sealed_invite_key_envelope - .unseal(org_key_slot, &mut context) - .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed) -} - -key_slot_ids! { - #[symmetric] - enum LocalSymmetricKeySlotId { - #[local] - Local(LocalId), - } - - #[private] - enum LocalPrivateKeySlotId { - #[local] - Local(LocalId), - } - - #[signing] - enum LocalSigningKeySlotId { - #[local] - Local(LocalId), - } - - LocalKeySlotIds => LocalSymmetricKeySlotId, LocalPrivateKeySlotId, LocalSigningKeySlotId; -} - -#[cfg(test)] -mod tests { - use bitwarden_crypto::SymmetricCryptoKey; - - use super::*; - - fn make_org_key() -> SymmetricCryptoKey { - SymmetricCryptoKey::make_aes256_cbc_hmac_key() - } - - #[test] - fn test_bundle_returns_valid_invite_key_and_envelope() { - let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); - // InviteKeyData serializes as a non-empty base64Url string - assert!(!String::from(&bundle.invite_key).is_empty()); - // InviteKeyEnvelope serializes as a non-empty EncString - assert!(!String::from(&bundle.sealed_invite_key_envelope).is_empty()); - } - - #[test] - fn test_envelope_unseals_to_raw_invite_key() { - let org_key_bytes = make_org_key().to_encoded().to_vec(); - let org_key = SymmetricCryptoKey::try_from( - &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key_bytes.clone()), - ) - .unwrap(); - let org_key_for_unseal = SymmetricCryptoKey::try_from( - &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key_bytes), - ) - .unwrap(); - - let bundle = generate_organization_invite_crypto_bundle(org_key).unwrap(); - - let unsealed = unseal_organization_invite_key( - org_key_for_unseal, - bundle.sealed_invite_key_envelope.clone(), - ) - .unwrap(); - - assert_eq!(bundle.invite_key, unsealed); - } - - #[test] - fn test_two_calls_produce_different_invite_keys() { - let org_key1 = make_org_key(); - let org_key2 = SymmetricCryptoKey::try_from( - &bitwarden_crypto::BitwardenLegacyKeyBytes::from(org_key1.to_encoded().to_vec()), - ) - .unwrap(); - let bundle1 = generate_organization_invite_crypto_bundle(org_key1).unwrap(); - let bundle2 = generate_organization_invite_crypto_bundle(org_key2).unwrap(); - assert_ne!( - String::from(&bundle1.invite_key), - String::from(&bundle2.invite_key) - ); - } - - #[test] - fn test_sealed_invite_key_envelope_is_encstring_text_format() { - // The server validates EncryptedInviteKey as a Bitwarden EncString text - // format (e.g. "2.iv|data|mac"). - let bundle = generate_organization_invite_crypto_bundle(make_org_key()).unwrap(); - let envelope_str = String::from(&bundle.sealed_invite_key_envelope); - assert!( - envelope_str.parse::().is_ok(), - "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope_str}" - ); - } - - #[test] - fn test_unseal_with_wrong_org_key_fails() { - let org_key_1 = make_org_key(); - let org_key_2 = make_org_key(); - - let bundle = generate_organization_invite_crypto_bundle(org_key_1).unwrap(); - - let result = unseal_organization_invite_key(org_key_2, bundle.sealed_invite_key_envelope); - - assert!( - result.is_err(), - "Unsealing with the wrong org key must fail" - ); - } -} diff --git a/crates/bitwarden-organization-invite-link/src/invite_link_client.rs b/crates/bitwarden-organization-invite-link/src/invite_link_client.rs new file mode 100644 index 0000000000..38f926d582 --- /dev/null +++ b/crates/bitwarden-organization-invite-link/src/invite_link_client.rs @@ -0,0 +1,214 @@ +use bitwarden_core::{ + Client, FromClient, OrganizationId, + key_management::{KeySlotIds, SymmetricKeySlotId}, +}; +use bitwarden_crypto::KeyStore; +use bitwarden_error::bitwarden_error; +use bitwarden_organization_crypto::{ + InviteKeyBundle, InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope, +}; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify::Tsify; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_INVITE_KEY_DATA: &'static str = r#" +export type InviteKeyData = Tagged; +"#; + +#[cfg(feature = "wasm")] +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_INVITE_KEY_ENVELOPE: &'static str = r#" +export type InviteKeyEnvelope = Tagged; +"#; + +/// Errors returned from [`InviteLinkClient`] operations. +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum OrganizationInviteCryptoBundleError { + /// Failed to generate the invite key bundle. + #[error("Key bundle generation failed: {0}")] + BundleGenerationFailed(#[from] InviteKeyBundleError), + /// Failed to unseal the invite key envelope using the organization key. + #[error("Failed to unseal invite key: {0}")] + UnsealingFailed(InviteKeyBundleError), +} + +/// The cryptographic bundle returned when generating an organization member invite link. +/// +/// - `invite_key`: raw invite key encoded as base64Url. **MUST NOT be sent to the server.** +/// - `sealed_invite_key_envelope`: invite key sealed with the organization key, serialized as an +/// EncString. Safe to send to the server. +#[derive(Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct OrganizationInviteCryptoBundle { + /// Raw invite key. CRITICAL: MUST NOT be sent to the server. + #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyData"))] + pub invite_key: InviteKeyData, + /// Invite key sealed with the organization key. Safe to send to the server. + #[cfg_attr(feature = "wasm", tsify(type = "InviteKeyEnvelope"))] + pub sealed_invite_key_envelope: InviteKeyEnvelope, +} + +/// Client for organization invite link cryptographic operations. +#[cfg_attr(feature = "wasm", wasm_bindgen)] +#[derive(FromClient)] +pub struct InviteLinkClient { + pub(crate) key_store: KeyStore, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl InviteLinkClient { + /// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the organization's key. + /// + /// The organization key is looked up from the client's key store via + /// [`SymmetricKeySlotId::Organization`]; the caller does not need to provide it directly. + /// + /// Each call produces a unique invite key sampled from a secure cryptographic source. + /// + /// # Security + /// The returned `invite_key` MUST NOT be sent to the server. + pub fn generate_invite_crypto_bundle( + &self, + organization_id: OrganizationId, + ) -> Result { + let mut ctx = self.key_store.context(); + let org_key = SymmetricKeySlotId::Organization(organization_id); + let bundle = InviteKeyBundle::make(org_key, &mut ctx)?; + Ok(OrganizationInviteCryptoBundle { + invite_key: bundle.dangerous_get_raw_invite_key().clone(), + sealed_invite_key_envelope: bundle.get_sealed_invite_key_envelope().clone(), + }) + } + + /// Unseals a `sealed_invite_key_envelope` using the organization's key, returning the raw + /// invite key as [`InviteKeyData`]. + pub fn unseal_invite_key( + &self, + organization_id: OrganizationId, + sealed_invite_key_envelope: InviteKeyEnvelope, + ) -> Result { + let mut ctx = self.key_store.context(); + let org_key = SymmetricKeySlotId::Organization(organization_id); + sealed_invite_key_envelope + .unseal(org_key, &mut ctx) + .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed) + } +} + +/// Extension trait that exposes [`InviteLinkClient`] on [`Client`]. +pub trait InviteLinkClientExt { + /// Returns an [`InviteLinkClient`] backed by this client's key store. + fn invite_link(&self) -> InviteLinkClient; +} + +impl InviteLinkClientExt for Client { + fn invite_link(&self) -> InviteLinkClient { + InviteLinkClient::from_client(self) + } +} + +#[cfg(test)] +mod tests { + use bitwarden_core::key_management::create_test_crypto_with_user_and_org_key; + use bitwarden_crypto::SymmetricCryptoKey; + + use super::*; + + fn make_client(org_id: OrganizationId) -> InviteLinkClient { + let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key); + InviteLinkClient { key_store } + } + + #[test] + fn generate_invite_crypto_bundle_returns_non_empty_fields() { + let org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let bundle = client.generate_invite_crypto_bundle(org_id).unwrap(); + + assert!(!String::from(&bundle.invite_key).is_empty()); + assert!(!String::from(&bundle.sealed_invite_key_envelope).is_empty()); + } + + #[test] + fn envelope_unseals_to_raw_invite_key() { + let org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let bundle = client.generate_invite_crypto_bundle(org_id).unwrap(); + let unsealed = client + .unseal_invite_key(org_id, bundle.sealed_invite_key_envelope.clone()) + .unwrap(); + + assert_eq!(bundle.invite_key, unsealed); + } + + #[test] + fn two_calls_produce_different_invite_keys() { + let org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let bundle1 = client.generate_invite_crypto_bundle(org_id).unwrap(); + let bundle2 = client.generate_invite_crypto_bundle(org_id).unwrap(); + + assert_ne!( + String::from(&bundle1.invite_key), + String::from(&bundle2.invite_key) + ); + } + + #[test] + fn sealed_envelope_serializes_as_encstring_text_format() { + // The server validates EncryptedInviteKey as a Bitwarden EncString text + // format (e.g. "2.iv|data|mac"). + let org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let bundle = client.generate_invite_crypto_bundle(org_id).unwrap(); + let envelope_str = String::from(&bundle.sealed_invite_key_envelope); + + assert!( + envelope_str.parse::().is_ok(), + "sealed_invite_key_envelope must parse as a valid EncString, got: {envelope_str}" + ); + } + + #[test] + fn unseal_with_wrong_organization_id_fails() { + let org_id = OrganizationId::new_v4(); + let other_org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let bundle = client.generate_invite_crypto_bundle(org_id).unwrap(); + let result = client.unseal_invite_key(other_org_id, bundle.sealed_invite_key_envelope); + + assert!(matches!( + result, + Err(OrganizationInviteCryptoBundleError::UnsealingFailed(_)) + )); + } + + #[test] + fn generate_with_unknown_organization_id_fails() { + let org_id = OrganizationId::new_v4(); + let other_org_id = OrganizationId::new_v4(); + let client = make_client(org_id); + + let result = client.generate_invite_crypto_bundle(other_org_id); + + assert!(matches!( + result, + Err(OrganizationInviteCryptoBundleError::BundleGenerationFailed( + _ + )) + )); + } +} diff --git a/crates/bitwarden-organization-invite-link/src/lib.rs b/crates/bitwarden-organization-invite-link/src/lib.rs index 3b422e766e..9572b0babe 100644 --- a/crates/bitwarden-organization-invite-link/src/lib.rs +++ b/crates/bitwarden-organization-invite-link/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] -mod invite; -pub use invite::{ - OrganizationInviteCryptoBundle, OrganizationInviteCryptoBundleError, - generate_organization_invite_crypto_bundle, unseal_organization_invite_key, +mod invite_link_client; +pub use invite_link_client::{ + InviteLinkClient, InviteLinkClientExt, OrganizationInviteCryptoBundle, + OrganizationInviteCryptoBundleError, }; diff --git a/crates/bitwarden-pm/Cargo.toml b/crates/bitwarden-pm/Cargo.toml index 6ad5c4aa05..8477d6751c 100644 --- a/crates/bitwarden-pm/Cargo.toml +++ b/crates/bitwarden-pm/Cargo.toml @@ -32,6 +32,7 @@ wasm = [ "bitwarden-core/wasm", "bitwarden-exporters/wasm", "bitwarden-generators/wasm", + "bitwarden-organization-invite-link/wasm", "bitwarden-policies/wasm", "bitwarden-send/wasm", "bitwarden-state/wasm", @@ -50,6 +51,7 @@ bitwarden-core = { workspace = true, features = ["internal"] } bitwarden-exporters = { workspace = true } bitwarden-fido = { workspace = true } bitwarden-generators = { workspace = true } +bitwarden-organization-invite-link = { workspace = true } bitwarden-policies = { workspace = true } bitwarden-send = { workspace = true } bitwarden-server-communication-config = { workspace = true } diff --git a/crates/bitwarden-pm/src/lib.rs b/crates/bitwarden-pm/src/lib.rs index 0310da32e5..1d2455cf9b 100644 --- a/crates/bitwarden-pm/src/lib.rs +++ b/crates/bitwarden-pm/src/lib.rs @@ -13,6 +13,7 @@ use bitwarden_core::{ }; use bitwarden_exporters::ExporterClientExt as _; use bitwarden_generators::GeneratorClientsExt as _; +use bitwarden_organization_invite_link::InviteLinkClientExt as _; use bitwarden_policies::PoliciesClientExt as _; use bitwarden_send::SendClientExt as _; use bitwarden_sync::SyncClientExt as _; @@ -28,6 +29,7 @@ pub mod clients { pub use bitwarden_core::key_management::CryptoClient; pub use bitwarden_exporters::ExporterClient; pub use bitwarden_generators::GeneratorClient; + pub use bitwarden_organization_invite_link::InviteLinkClient; pub use bitwarden_policies::PolicyClient; pub use bitwarden_send::SendClient; pub use bitwarden_sync::SyncClient; @@ -144,6 +146,11 @@ impl PasswordManagerClient { self.0.policies() } + /// Organization invite link operations + pub fn invite_link(&self) -> bitwarden_organization_invite_link::InviteLinkClient { + self.0.invite_link() + } + /// Sync operations pub fn sync(&self) -> bitwarden_sync::SyncClient { self.0.sync() diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 4227e24a89..ad4b27364f 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -117,6 +117,11 @@ impl PasswordManagerClient { pub fn sends(&self) -> SendClient { self.0.sends() } + + /// Organization invite link operations. + pub fn invite_link(&self) -> InviteLinkClient { + self.0.invite_link() + } } #[bitwarden_error(basic)] From d7d3133baee69e8d4933c7188cf59db327591a4f Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 9 Jun 2026 16:36:11 -0400 Subject: [PATCH 09/11] fix duplicate types --- .../bitwarden-organization-crypto/src/wasm.rs | 4 ++++ .../src/invite_link_client.rs | 18 +++--------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index 83543c29c4..2469974312 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -11,6 +11,10 @@ use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi}; use crate::{InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope}; +/// WASM bindings for organization cryptography operations. +#[wasm_bindgen::prelude::wasm_bindgen] +pub struct OrganizationCryptoWasm; + #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] const TS_INVITE_KEY_DATA: &'static str = r#" export type InviteKeyData = Tagged; diff --git a/crates/bitwarden-organization-invite-link/src/invite_link_client.rs b/crates/bitwarden-organization-invite-link/src/invite_link_client.rs index 38f926d582..cad1fcb0ea 100644 --- a/crates/bitwarden-organization-invite-link/src/invite_link_client.rs +++ b/crates/bitwarden-organization-invite-link/src/invite_link_client.rs @@ -14,18 +14,6 @@ use tsify::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::wasm_bindgen; -#[cfg(feature = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_INVITE_KEY_DATA: &'static str = r#" -export type InviteKeyData = Tagged; -"#; - -#[cfg(feature = "wasm")] -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_INVITE_KEY_ENVELOPE: &'static str = r#" -export type InviteKeyEnvelope = Tagged; -"#; - /// Errors returned from [`InviteLinkClient`] operations. #[bitwarden_error(flat)] #[derive(Debug, Error)] @@ -116,13 +104,13 @@ impl InviteLinkClientExt for Client { #[cfg(test)] mod tests { use bitwarden_core::key_management::create_test_crypto_with_user_and_org_key; - use bitwarden_crypto::SymmetricCryptoKey; + use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm}; use super::*; fn make_client(org_id: OrganizationId) -> InviteLinkClient { - let user_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); - let org_key = SymmetricCryptoKey::make_aes256_cbc_hmac_key(); + let user_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); + let org_key = SymmetricCryptoKey::make(SymmetricKeyAlgorithm::Aes256CbcHmac); let key_store = create_test_crypto_with_user_and_org_key(user_key, org_id, org_key); InviteLinkClient { key_store } } From dc7178f3ac282a0e36a3fd8e87ac43c546fb4c8c Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 11 Jun 2026 13:11:15 -0400 Subject: [PATCH 10/11] move ts custome type decs next to their respective struts so linker doesn't drop them --- .../src/invite_key_bundle.rs | 10 ++++++++++ crates/bitwarden-organization-crypto/src/wasm.rs | 14 -------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs index c66ad61d88..6118458168 100644 --- a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs +++ b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs @@ -25,6 +25,11 @@ pub enum InviteKeyBundleError { MissingKeyId(String), } +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_INVITE_KEY_DATA: &'static str = r#" +export type InviteKeyData = Tagged; +"#; + /// Struct for holding the Invite Key's raw byte data. Supports WASM bindings, /// automatically using base64Url encoding for both `wasm-bindgen` and `tsify`. /// @@ -91,6 +96,11 @@ impl Serialize for InviteKeyData { } } +#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] +const TS_INVITE_KEY_ENVELOPE: &'static str = r#" +export type InviteKeyEnvelope = Tagged; +"#; + /// Struct for holding the wrapped invite key data. Currently supports encstring /// but the inner type must remain private as it may be extended in the future. #[derive(Clone)] diff --git a/crates/bitwarden-organization-crypto/src/wasm.rs b/crates/bitwarden-organization-crypto/src/wasm.rs index 2469974312..5bf68cf1fb 100644 --- a/crates/bitwarden-organization-crypto/src/wasm.rs +++ b/crates/bitwarden-organization-crypto/src/wasm.rs @@ -11,20 +11,6 @@ use wasm_bindgen::convert::{FromWasmAbi, IntoWasmAbi, OptionFromWasmAbi}; use crate::{InviteKeyBundleError, InviteKeyData, InviteKeyEnvelope}; -/// WASM bindings for organization cryptography operations. -#[wasm_bindgen::prelude::wasm_bindgen] -pub struct OrganizationCryptoWasm; - -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_INVITE_KEY_DATA: &'static str = r#" -export type InviteKeyData = Tagged; -"#; - -#[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] -const TS_INVITE_KEY_ENVELOPE: &'static str = r#" -export type InviteKeyEnvelope = Tagged; -"#; - impl wasm_bindgen::describe::WasmDescribe for InviteKeyData { fn describe() { ::describe(); From a6cc4f42dca6018e6c17b9088f09db533330c068 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 11 Jun 2026 13:23:27 -0400 Subject: [PATCH 11/11] add cfg[feature(wasm)] to ts custom types --- crates/bitwarden-organization-crypto/src/invite_key_bundle.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs index 6118458168..4acbb00765 100644 --- a/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs +++ b/crates/bitwarden-organization-crypto/src/invite_key_bundle.rs @@ -25,6 +25,7 @@ pub enum InviteKeyBundleError { MissingKeyId(String), } +#[cfg(feature = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] const TS_INVITE_KEY_DATA: &'static str = r#" export type InviteKeyData = Tagged; @@ -96,6 +97,7 @@ impl Serialize for InviteKeyData { } } +#[cfg(feature = "wasm")] #[wasm_bindgen::prelude::wasm_bindgen(typescript_custom_section)] const TS_INVITE_KEY_ENVELOPE: &'static str = r#" export type InviteKeyEnvelope = Tagged;