Skip to content

[PM-35796] Invite link Crypto Bundle Generation and Unsealing#1157

Open
BTreston wants to merge 4 commits into
mainfrom
ac/pm-35796-organization-invite-link-crypto
Open

[PM-35796] Invite link Crypto Bundle Generation and Unsealing#1157
BTreston wants to merge 4 commits into
mainfrom
ac/pm-35796-organization-invite-link-crypto

Conversation

@BTreston
Copy link
Copy Markdown

@BTreston BTreston commented Jun 2, 2026

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-35796

📔 Objective

Create the admin console owned crate in the SDK + WASM bindings that call the new KM interface InviteKeyBundle. Also edits InviteKeyBundle to return an EncString instead of B64.

Wired up in the clients in bitwarden/clients#21004

🚨 Breaking Changes

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🔍 SDK Breaking Change Detection

SDK Version: ac/pm-35796-organization-invite-link-crypto (1710670)

⚠️ If breaking changes are detected, a corresponding pull request addressing them must be ready for merge in the affected client repository.

Client Status Details
typescript ✅ No breaking changes detected Compilation passed with new SDK version - View Details
android ✅ No breaking changes detected Compilation passed with new SDK version - View Details

Breaking change detection uses the build of the SDK from this branch, including any incompatibities pre-existing on or merged into this branch. Check the workflow logs to confirm.
Results update as workflows complete.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 96.42857% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.15%. Comparing base (607ceb6) to head (a77ee1a).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
...arden-organization-crypto/src/invite_key_bundle.rs 70.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1157      +/-   ##
==========================================
+ Coverage   83.89%   84.15%   +0.26%     
==========================================
  Files         441      447       +6     
  Lines       57844    58890    +1046     
==========================================
+ Hits        48528    49560    +1032     
- Misses       9316     9330      +14     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@BTreston BTreston added the ai-review Request a Claude code review label Jun 2, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE with minor follow-ups

This PR introduces the bitwarden-organization-invite-link crate (admin-console-owned) with generate_organization_invite_crypto_bundle and unseal_organization_invite_key wasm bindings, and switches InviteKeyEnvelope's wire format from a base64-wrapped buffer to the canonical EncString text format ("2.iv|data|mac") that the server's EncryptedInviteKey validator expects. The latest commit also tightened the new API to take SymmetricCryptoKey and InviteKeyEnvelope newtypes instead of raw String/Vec<u8>, addressing the prior review concerns from quexten. Test coverage includes round-trip unsealing, distinctness across calls, EncString format compliance, and wrong-key rejection.

Code Review Details
  • ♻️ : InvalidSealedEnvelope error variant is dead code (never constructed)
    • crates/bitwarden-organization-invite-link/src/invite.rs:40
  • 🎨 : Return InviteKeyData (redacted Debug) instead of bare String from unseal_organization_invite_key
    • crates/bitwarden-organization-invite-link/src/invite.rs:98

@BTreston
Copy link
Copy Markdown
Author

BTreston commented Jun 2, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Jun 2, 2026

Claude finished @BTreston's task in 3m 11s —— View job


Claude Code Review

Scope reviewed: New bitwarden-organization-invite-link crate, simplified InviteKeyEnvelope serialization in bitwarden-organization-crypto, and the WASM re-export wiring.

Overall this is a clean, well-tested PR. Tests cover the round-trip, non-determinism, EncString-format validation, invalid org key, and wrong-key unsealing — nice coverage. A few items below worth considering.


🟠 Sensitive material exposed via Serialize / tsify(from_wasm_abi)

crates/bitwarden-organization-invite-link/src/invite.rs:31-39

#[derive(Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct OrganizationInviteCryptoBundle {
    pub invite_key: String,
    pub sealed_invite_key_envelope: String,
}

You took care to redact invite_key in Debug (good), but Serialize still outputs the raw key in plain text. Any consumer that logs the JSON-serialized form (e.g. tracing::debug!(?bundle) swapped to serde_json::to_string(&bundle), or a generic error tap that includes serialized state) would leak the URL key.

Two related nits:

  • tsify(from_wasm_abi) adds inbound conversion. Per the doc comment, this struct is output-only (produced by generate_organization_invite_crypto_bundle). Consider tsify(into_wasm_abi) only.
  • Deserialize is similarly unused outside WASM. Removing it would shrink the surface area for accidental round-tripping.

If a JSON shape is genuinely required for WASM transport, that's the tradeoff — but flagging it because the redacted Debug shows you noticed the risk, and Serialize is the larger blast radius. A future improvement could be a newtype with a serialize-time guard or explicit into_raw() extraction.

🟡 Test gap: malformed sealed envelope

crates/bitwarden-organization-invite-link/src/invite.rs:178-200

You have coverage for an invalid org key and an unsealing failure with the wrong org key, but no test that a malformed sealed_invite_key_envelope (e.g. "not-an-encstring") returns OrganizationInviteCryptoBundleError::InvalidSealedEnvelope. Worth ~5 lines:

#[test]
fn test_unseal_with_malformed_envelope_returns_invalid_sealed_envelope() {
    let result = unseal_organization_invite_key(make_org_key(), "not-an-encstring".into());
    assert!(matches!(
        result,
        Err(OrganizationInviteCryptoBundleError::InvalidSealedEnvelope(_))
    ));
}

🟡 Document expected envelope format on unseal_organization_invite_key

crates/bitwarden-organization-invite-link/src/invite.rs:78-86

The struct doc comment on OrganizationInviteCryptoBundle mentions the "2.iv|data|mac" EncString text format, but the standalone unseal_organization_invite_key doc doesn't. Since this function is called independently (e.g. from reconstructUrl), spelling out the expected input format in its doc comment helps WASM/TS callers without cross-referencing the bundle type.

🔵 #[allow(missing_docs)] on a public error enum

crates/bitwarden-organization-invite-link/src/invite.rs:11-24

Each variant already has a #[error("...")] string, so a one-line /// per variant would render in docs and you could drop the allow. Minor consistency item with the rest of the crate's documentation effort.

🔵 PR body is empty

The Objective and Breaking Changes sections in the PR description are blank. Worth filling in — particularly because the change to InviteKeyEnvelope's From<&_> for String and FromStr is a serialization format change (base64-wrapped buffer → EncString text format "2.iv|data|mac"). If anything has already persisted invite envelopes in the old format, that won't round-trip. Looks like the original was only introduced in a prior unreleased PR so probably moot, but worth confirming in the description.

🟢 Nits

  • unseal_organization_invite_key takes sealed_invite_key_envelope: String but only borrows it via &sealed_invite_key_envelope. Could be &str — though for WASM ABI consistency String is fine.
  • The triple of LocalSymmetricKeySlotId / LocalPrivateKeySlotId / LocalSigningKeySlotId is required by the KeySlotIds trait but only the symmetric slot is exercised. Not a defect; flagging in case a brief comment in key_slot_ids! would help future readers.
  • crates/bitwarden-organization-crypto/src/invite_key_bundle.rs:98-99 — nice that the new doc comment on From<&InviteKeyEnvelope> for String calls out the server validator's expected format.

Summary

Solid implementation with thoughtful documentation around the dangerous raw-key handling. The main item I'd ask you to consider is whether the Serialize/from_wasm_abi surface is intentional given the Debug redaction. The test gap and doc tweaks are quick wins.

  • Read PR diff and identify changed files
  • Review new crate bitwarden-organization-invite-link
  • Review changes to invite_key_bundle.rs
  • Review Cargo.toml/wasm bindings updates
  • Check tests, security, and best practices
  • Post review findings
    • Branch: ac/pm-35796-organization-invite-link-crypto

@BTreston BTreston marked this pull request as ready for review June 2, 2026 19:04
@BTreston BTreston requested review from a team as code owners June 2, 2026 19:04
.expect("`to_buffer` never fails for `EncString`"),
)
.to_string()
key_data.0.to_string()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: Sibling docs/examples still describe the old base64 format for InviteKeyEnvelope.

Details and fix

This PR changed From<&InviteKeyEnvelope> for String and FromStr from the B64-of-buffer encoding to the EncString text format ("2.iv|data|mac"), but two adjacent locations in the same crate still describe the prior behavior:

  1. crates/bitwarden-organization-crypto/src/wasm.rs:1-6 — module-level doc says: "base64url for the InviteKeyData type, and base64 for the InviteKey type". The "base64 for the InviteKey" half no longer applies — the IntoWasmAbi/FromWasmAbi impls now serialize via the new EncString-text format.

  2. crates/bitwarden-organization-crypto/examples/make_invite_key_bundle.rs:32-36 — comment says InviteKeyEnvelope "automatically serializes to base64 when using serde, String::from(&inviteKeyEnvelope), or wasm abi serialization." This is now misleading.

Updating these to reference the EncString text format will keep the crate's documentation consistent with its new serialization contract (and with the new doc comment you added on From<&InviteKeyEnvelope> for String).

@BTreston BTreston requested a review from a team as a code owner June 2, 2026 19:50
coroiu
coroiu previously approved these changes Jun 3, 2026
Copy link
Copy Markdown
Contributor

@coroiu coroiu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bitwarden-organization-invite-link seems very specific 🤷

#[serde(rename_all = "camelCase")]
pub struct OrganizationInviteCryptoBundle {
/// Raw invite key as base64Url. CRITICAL: MUST NOT be sent to the server.
pub invite_key: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub invite_key: String,
pub invite_key: SymmetricKey,

/// 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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub sealed_invite_key_envelope: String,
pub sealed_invite_key_envelope: EncString,

Ideally the newtype should actually be wasm exposed. I'm not sure why we didn't in the first place. But we should always use as specific of a type as possible, even if they are newtypes around string.

On the WASM side you can still directly pass the generated type into functions expecting string, though using a newtype there is also preferred.

Comment thread crates/bitwarden-organization-invite-link/src/invite.rs
use tsify::Tsify;

/// Errors from generating an organization invite crypto bundle.
#[allow(missing_docs)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#[allow(missing_docs)]

Please add docs. The missing docs allow is only meant for cases where we did not yet provide docs.

impl std::fmt::Debug for OrganizationInviteCryptoBundle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OrganizationInviteCryptoBundle")
.field("invite_key", &"<REDACTED>")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If invite key were using the proper symmetrickey type, this would not be needed because symmetrickey already implemnets debug and prevents you from logging it. That's why using string here is dangerous / relies on you not logging it.


/// Generates a new [`OrganizationInviteCryptoBundle`] sealed with the provided organization key.
///
/// Each call produces a unique, non-deterministic invite key.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Each call produces a unique, non-deterministic invite key.
/// Each call produces a unique invite key sampled from a secure cryptographic source.

/// 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<u8>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
org_key: Vec<u8>,
org_key: &SymmetricKey,

Please do not use Vec<u8> or String to represent keys. We want to use the SymmetricKey type for this.

Comment on lines +84 to +85
org_key: Vec<u8>,
sealed_invite_key_envelope: String,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
org_key: Vec<u8>,
sealed_invite_key_envelope: String,
org_key: SymmetricKey,
sealed_invite_key_envelope: &EncString, // OR &InviteKeyEnvelope??

@quexten
Copy link
Copy Markdown
Contributor

quexten commented Jun 3, 2026

@BTreston Looks mostly good, but we have a few times where we are using generic types (String, Vec) when we can use the newtypes (SymmetricKey, EncString). These are preferred because the provide better type safety, but also prevent dangerous logging of cryptographic material.

Comment on lines +38 to +40
#[error("Invalid sealed invite key envelope: {0}")]
/// Invalid Envelope
InvalidSealedEnvelope(InviteKeyBundleError),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: InvalidSealedEnvelope variant is never constructed and is unreachable dead code.

Details and fix

This variant is declared on OrganizationInviteCryptoBundleError but no code path constructs it. The two real failure paths use BundleGenerationFailed (via #[from]) and UnsealingFailed (mapped explicitly in unseal_organization_invite_key). Envelope parsing happens in the wasm from_abi, which calls unwrap_throw() rather than surfacing a typed error.

Suggested change
#[error("Invalid sealed invite key envelope: {0}")]
/// Invalid Envelope
InvalidSealedEnvelope(InviteKeyBundleError),
#[error("Failed to unseal invite key: {0}")]
/// Unsealing Envelope failed
UnsealingFailed(InviteKeyBundleError),
}

Since this is exposed in the #[bitwarden_error(flat)] enum, an unused variant also leaks into the generated TypeScript surface as a value that JS callers can never actually receive. Either drop the variant or wire it into a real parse path.

Comment on lines +95 to +98
pub fn unseal_organization_invite_key(
org_key: SymmetricCryptoKey,
sealed_invite_key_envelope: InviteKeyEnvelope,
) -> Result<String, OrganizationInviteCryptoBundleError> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: Return InviteKeyData instead of bare String to preserve the redacted Debug impl.

Details and fix

The function currently unseals the envelope and immediately calls String::from(&invite_key_data), returning the raw invite key as an unprotected String. This is the same class of concern that motivated using SymmetricCryptoKey / InviteKeyEnvelope newtypes for the inputs in the latest commit: InviteKeyData has a redacted Debug impl (delegates to SymmetricCryptoKey::fmt), but a String does not — any future Rust caller (mobile/desktop) that logs the result would leak the raw key material.

The wasm ABI for InviteKeyData already serializes to a base64Url string, so the TypeScript-side surface stays identical:

pub fn unseal_organization_invite_key(
    org_key: SymmetricCryptoKey,
    sealed_invite_key_envelope: InviteKeyEnvelope,
) -> Result<InviteKeyData, OrganizationInviteCryptoBundleError> {
    // ...
    sealed_invite_key_envelope
        .unseal(org_key_slot, &mut context)
        .map_err(OrganizationInviteCryptoBundleError::UnsealingFailed)
}

The TS signature in TS_FUNCTIONS would then change its return type from string to InviteKeyData (still a tagged string), matching how the bundle's invite_key field is already typed.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 3, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants