Skip to content

Auth/PM-40520 - Registration - Open Org Invite Seal / Unseal Support - #1295

Merged
JaredSnider-Bitwarden merged 40 commits into
mainfrom
auth/pm-40520/registration-open-org-invite-seal-unseal
Jul 29, 2026
Merged

Auth/PM-40520 - Registration - Open Org Invite Seal / Unseal Support #1295
JaredSnider-Bitwarden merged 40 commits into
mainfrom
auth/pm-40520/registration-open-org-invite-seal-unseal

Conversation

@JaredSnider-Bitwarden

@JaredSnider-Bitwarden JaredSnider-Bitwarden commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-40520
Tech breakdown: https://github.com/bitwarden/tech-breakdowns/tree/main/auth/PM-39706-open-invite-registration-crossing

📔 Objective

To provide a simple API on the Auth registration client to seal and unseal the new open organization invite link data so it can be safely carried through the registration with email verification process.

The type had no cross-boundary API — its bytes were pub(crate) and the WASM
ABI encoded them inline. Our first consumer of SecretProtectedKeyEnvelope
outside bitwarden-crypto needs to embed the secret in a serde-serialized
wire artifact (a struct crossing the WASM boundary alongside a paired sealed
data blob), so surface the same base64 conversion the WASM ABI already uses
as Rust API.
Two new production namespace variants, plus round-trip discriminant tests and
a cross-namespace unseal test that mirrors test_namespace_cross_contamination_protection
against the new DataEnvelopeNamespace::RegistrationOpenOrgInviteData variant.

Discriminant values are load-bearing wire markers — once shipped they cannot
be renumbered without invalidating every envelope in flight, hence the
explicit discriminant assertions.
…ssing

Two new methods on the existing bitwarden-auth RegistrationClient carry an
open-organization-invite context across the anonymous verification-email tab
boundary that opens between registration-start and registration-finish.

Construction:
- Inner DataEnvelope seals a versioned payload ({organization_id,
  invite_link_code, invite_key}) under a freshly generated AES-256-GCM CEK.
- Outer SecretProtectedKeyEnvelope seals that CEK under a per-registration
  HighEntropySecret via HKDF-SHA-256.
- Wire format: CBOR{data_envelope, key_envelope} base64url-encoded, safe
  for the verification-email URL fragment.

The seal method returns (sealed_data, high_entropy_secret) as strings — the
sealed data is server-visible and rides the email, the secret stays
client-side. Both halves are needed to unseal; substitution defense at both
layers is the AES-GCM auth-tag / wrong-key check.

The CEK never persists between calls: seal and unseal each create a
transient KeyStore<KeySlotIds> scoped to the operation.

Also mirrored the invite-link WASM integration test pattern to prove the
FFI boundary carries snake_case fields intact and per-registration
randomness across two independent seals.
Copilot AI review requested due to automatic review settings July 21, 2026 23:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds support for sealing/unsealing an “open organization invite” context across the registration → email-verification boundary using a two-envelope crypto construction, with Rust + WASM integration coverage.

Changes:

  • Introduces RegistrationClient::seal_open_org_invite_data / unseal_open_org_invite_data and related wire framing (CBOR wrapped + base64url).
  • Adds new DataEnvelopeNamespace and SecretProtectedKeyEnvelopeNamespace discriminants with regression tests to lock their wire values.
  • Extends HighEntropySecret with base64 encode/decode helpers and adds WASM integration tests for round-tripping.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
crates/bitwarden-wasm-internal/integration-tests/tests/invite/open-org-invite-registration.test.ts New WASM integration tests covering seal/unseal and base64url round-trip behavior.
crates/bitwarden-crypto/src/safe/secret_protected_key_envelope.rs Adds a new envelope namespace discriminant plus mapping tests.
crates/bitwarden-crypto/src/safe/high_entropy_secret.rs Adds base64 encode/decode helpers and tests; extends error enum.
crates/bitwarden-crypto/src/safe/data_envelope.rs Adds a new data envelope namespace discriminant plus mapping/cross-namespace tests.
crates/bitwarden-auth/src/registration/unseal_open_org_invite_data.rs New unseal implementation for the registration invite context.
crates/bitwarden-auth/src/registration/seal_open_org_invite_data.rs New seal implementation + types for the registration invite context.
crates/bitwarden-auth/src/registration/open_org_invite_data.rs New versioned, sealable payload for the invite context.
crates/bitwarden-auth/src/registration/mod.rs Wires new registration modules and re-exports new public types.
crates/bitwarden-auth/Cargo.toml Adds ciborium dependency and normalizes feature formatting.
Cargo.lock Locks in the new ciborium dependency for bitwarden-auth.

Comment thread crates/bitwarden-auth/src/registration/seal_open_org_invite_data.rs Outdated
Comment thread crates/bitwarden-auth/src/registration/seal_open_org_invite_data.rs Outdated
Comment thread crates/bitwarden-auth/src/registration/open_org_invite_data.rs Outdated
Comment thread crates/bitwarden-crypto/src/safe/high_entropy_secret.rs Outdated
Comment thread crates/bitwarden-crypto/src/safe/high_entropy_secret.rs
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🔍 SDK Breaking Change Detection

SDK Version: auth/pm-40520/registration-open-org-invite-seal-unseal (b424f8f)

⚠️ 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.

@JaredSnider-Bitwarden JaredSnider-Bitwarden added the ai-review Request a Claude code review label Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Re-review at abc18104. The only change since the previous pass (8518461f) is the .github/CODEOWNERS restoration, and it is correct: git diff origin/main HEAD -- .github/CODEOWNERS now shows exactly one added line (crates/bitwarden-wasm-internal/integration-tests/tests/registration/** @bitwarden/team-auth-dev), with the * @bitwarden/team-platform-dev default owner and all six previously backslash-escaped globs (bitwarden-api-key-connector/src/**, bw/src/vault/**, publish-*.yml, build-*, **/*.dockerignore, **/uniffi.toml) back to their unescaped form. I also re-verified the full crossing at HEAD — the open_org_invite_crypto/ module layout, DataEnvelope/SecretProtectedKeyEnvelope namespace discriminants (both = 3, no collision on main), the CBOR + base64url wire encoding, the HighEntropySecret FromStr/Serialize pattern with its MIN_SECRET_LENGTH floor, and the pinned wire test vector — and found no new issues.

Code Review Details

No findings.

All 11 unresolved threads are marked outdated and were addressed by later commits (camelCase serde, serde_bytes on the CBOR byte fields, the DataEnvelope trait-pattern refactor, module restructure into mod.rs / data_v1.rs / open_org_invite.rs / serialization.rs / client.rs, crate-level integration tests plus pinned vector, and the CODEOWNERS fix). The declined items (.unwrap() in #[cfg(test)], .expect(...) in From<&SealedOpenOrgInviteData> for String, the HighEntropySecretError variant objection, and the seal/unseal file split) are not re-raised — the code at HEAD is consistent with those resolutions.

ciborium was added to crates/bitwarden-auth/Cargo.toml, but it is an already-approved workspace dependency (Cargo.toml:78, ciborium = ">=0.2.2, <0.3"), not a net-new package, so no AppSec approval finding applies. Cargo.lock was updated consistently with the manifest change.

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.72141% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.86%. Comparing base (a91e406) to head (abc1810).

Files with missing lines Patch % Lines
...gistration/open_org_invite_crypto/serialization.rs 83.15% 16 Missing ⚠️
...s/bitwarden-crypto/src/safe/high_entropy_secret.rs 96.36% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1295      +/-   ##
==========================================
+ Coverage   85.81%   85.86%   +0.04%     
==========================================
  Files         490      493       +3     
  Lines       70590    70929     +339     
==========================================
+ Hits        60574    60900     +326     
- Misses      10016    10029      +13     

☔ View full report in Codecov by Harness.
📢 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.

…ration/

The sibling tests/invite/ subtree is admin-console-owned via .github/CODEOWNERS,
so placing this file there auto-added admin-console as reviewers for an auth
feature. Move to a new tests/registration/ subdirectory and add a matching
CODEOWNERS entry routing it to team-auth-dev, mirroring how tests/unlock/**
is KM-owned.
Companion to the preceding move of open-org-invite-registration.test.ts into
tests/registration/. Adds an entry alongside the existing tests/invite/**
(admin-console) and tests/unlock/** (KM) lines so review of auth-owned WASM
integration tests routes to team-auth-dev.
Copilot AI review requested due to automatic review settings July 22, 2026 00:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.

Comment thread crates/bitwarden-crypto/src/safe/secret_protected_key_envelope.rs
Comment thread crates/bitwarden-crypto/src/safe/data_envelope.rs Outdated
Comment thread crates/bitwarden-crypto/src/safe/data_envelope.rs Outdated
Comment thread crates/bitwarden-crypto/src/safe/data_envelope.rs
Comment thread crates/bitwarden-crypto/src/safe/high_entropy_secret.rs Outdated
Comment thread crates/bitwarden-auth/src/registration/open_org_invite/unseal.rs Outdated
Copilot review flagged that OpenOrgInviteData, SealedOpenOrgInvite, and the
inner RegistrationOpenOrgInviteDataV1 payload were emitting snake_case fields
across the WASM boundary, inconsistent with LoginRequest and every other
recent bitwarden-auth model (e.g. login_via_password, send_access). The
JitMasterPasswordRegistrationRequest siblings I originally mirrored are the
outlier, not the pattern.

Since none of these types have shipped, aligning them now avoids setting a
new snake_case precedent on the WASM boundary and matches what the client-
side TS caller (PM-39706) will actually want to consume. The inner V1 payload
is the sealed CBOR wire format — no compatibility risk because no sealed
blobs exist in the wild yet.

Also simplifies the base64url round-trip check in the integration test to use
Node's native "base64url" Buffer encoding (available since Node 16, which CI
uses) instead of the manual `+`/`-`, `/`/`_`, and padding substitutions, and
drops `await` on the now-synchronous WASM methods.
Copilot review noted the new base64 constructor bypassed the type's own 16-byte
floor: any successfully-decoded input, however short, could produce a
`HighEntropySecret` that lied about the length invariant the KDF assumes.

Add the same MIN_SECRET_LENGTH check `make(...)` enforces so all constructors
honor the same floor. The check is a floor, not an entropy check — a 32-byte
ASCII string is still accepted (as documented) — but it closes the trivially-
short-input gap.

Also adds two guard tests (below-minimum lengths rejected as TooShort, and
input at exactly MIN_SECRET_LENGTH accepted).
Copilot AI review requested due to automatic review settings July 22, 2026 01:26
Comment thread crates/bitwarden-auth/src/registration/open_org_invite/seal.rs Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

crates/bitwarden-crypto/src/safe/high_entropy_secret.rs:12

  • rand::Rng is imported but never used in this module, which will trigger an unused import warning (and may fail CI if warnings are denied).
use bitwarden_encoding::B64;
use bitwarden_sensitive_value::{ExposeSensitive, Sensitive, SensitiveSlice};
use rand::Rng;
use thiserror::Error;

Comment thread crates/bitwarden-auth/src/registration/open_org_invite_data.rs Outdated
Reviewer caught that the doc comment claimed `bstr` semantics but the code was
relying on serde's default `Vec<u8>` handling, which ciborium serializes as a
CBOR array of integers (one item per byte) rather than a byte string. Round-
tripping still worked because both sides used the same shape, but the encoded
sealed_data was roughly 2x larger than the comment implied — and the wire
format is load-bearing per the namespace-discriminant tests once it ships.

Fix: `#[serde(with = "serde_bytes")]` on both fields so serde honors the
bstr intent; existing round-trip test proves the change stays correct.

Also rewrote the doc comment to accurately describe why the attribute is
needed (so future readers don't remove it thinking it's redundant).
Copilot AI review requested due to automatic review settings July 22, 2026 01:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.

Reviewer flagged that `open_org_invite_data.rs` was implementing
`SealableVersionedData` manually, which the trait docs explicitly say not to
do. The reason was structural: the macro emits `enum $enum_name` with no
visibility keyword, so the generated enum is module-private and can't be
shared across the sibling `seal_open_org_invite_data` / `unseal_open_org_invite_data`
modules that both need it.

Adding a single `$vis:vis` matcher in front of the enum name in the macro
lets callers optionally opt into a specific visibility (`pub`, `pub(crate)`,
`pub(super)`, etc.) while leaving every existing caller untouched — the empty
match is still valid. Doc comment updated to describe the new prefix.

With that in place, `open_org_invite_data.rs` now uses the macro properly,
which drops ~15 lines of hand-written boilerplate that duplicated the macro's
output. Renamed the variant match in the unseal path to match the macro's
naming convention (variant name = struct name), matching how future new
versions would be added.
Copilot AI review requested due to automatic review settings July 22, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated no new comments.

Reorganize the open-organization-invite registration crossing from three
flat sibling files (open_org_invite_data.rs, seal_open_org_invite_data.rs,
unseal_open_org_invite_data.rs) into a single open_org_invite/ module:

- mod.rs       — versioned enum via generate_versioned_sealable!, re-exports
- wire_v1.rs   — RegistrationOpenOrgInviteDataV1 wire schema
- seal.rs      — public request/response types + seal path + shared helpers
- unseal.rs    — unseal path + framing split helper

With seal/unseal as children of open_org_invite instead of top-level
siblings, the versioned enum can stay module-private (visible to descendants)
and the macro no longer needs a $vis:vis prefix. Reverts the macro visibility
change from bd2af69 in bitwarden-crypto; that capability wasn't mine to
add and the folder shape removes the need for it. Follow-up to the layout
alternative Copilot suggested on #1295 (relocate vs. teach the macro).

Renamed the seal-side public input `OpenOrgInviteData` to
`OpenOrgInviteSealRequest` per the SDK's [Noun][Op]Request convention in
CLAUDE.md. `SealedOpenOrgInvite` keeps its shape-neutral name and now
carries a doc explaining it is both the seal response and the unseal
request — either suffix would misrepresent one of the two roles.
Per PR feedback that some comments were noisy enough to distract rather
than inform. Drops redundant test-body preambles (test names already
describe intent), shortens defensive "a future refactor would break
this" commentary, condenses parallel KeyStore-scoping comments in seal
and unseal, and removes an external doc-reference ("AC 1.iii analogue")
from a test.

No behavior change; wire format unchanged.
…ation-open-org-invite-seal-unseal + merge conflict fixes
quexten
quexten previously approved these changes Jul 27, 2026

@quexten quexten left a comment

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.

Approving KM owned code. Please conisder the advice on structure. While it is optional, I do feel that the current implementation is an anti-pattern, as we are splitting the implementation of a struct's logic, and moving it into clients across multiple files, when not necessary.

This would be greatly simplified by moving the logic onto impl's on the struct, then making the client calls much lighter (and merging the files for the client functions, since now they are tiny).

Per PR review, RegistrationClient methods were orchestrating the crypto
and constructing the sealed struct field-by-field, breaking encapsulation
of the domain type. Crypto now lives on SealedOpenOrgInviteData::seal /
::unseal; the client methods are thin FFI wrappers.

Also splits the module by concern:
- open_org_invite.rs  — domain types + crypto impls + round-trip tests
- serialization.rs    — wire encoding + WASM ABI + wire/parse tests
- client.rs           — FFI pair struct + thin RegistrationClient wrappers

SealedOpenOrgInviteData's envelope fields tightened from pub to pub(super)
since the client no longer needs field access. Folder renamed to
open_org_invite_crypto to satisfy clippy's module_inception check.

Wire format unchanged (pinned integration-test vector still passes).
Public API surface unchanged.
Comment thread crates/bitwarden-auth/src/registration/open_org_invite_crypto/serialization.rs Outdated
…ation-open-org-invite-seal-unseal + codeowners merge conflict fix
Comment thread .github/CODEOWNERS Outdated
…hrough Prettier

Merge commit 8518461 accidentally reformatted CODEOWNERS as Markdown, which
turned `*` into `-` (dropping the repo-wide default owner), backslash-escaped
six glob patterns (breaking gitignore-style matching for API key connector,
BW CLI vault, publish/build workflows, dockerignore, and uniffi.toml), and
inserted blank lines after every comment header. Restore the file to
origin/main and re-apply only the one intended line for the new
tests/registration/** ownership.
#[serde(rename = "d", with = "serde_bytes")]
data_envelope: Vec<u8>,
#[serde(rename = "k", with = "serde_bytes")]
key_envelope: 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.

Nit: Naming this "sealed_data_envelope_cek" would be clearer. It is the CEK of the data envelope.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Will address in fast follow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Actually, the related type is called SecretProtectedKeyEnvelope? I'm mirroring KM naming here. I've added an additional comment in the follow up work to clarify, but respectfully, I'm not seeing a benefit of the other name. Sealed is already in the struct name SealedOpenOrgInviteDataWire. It's an envelope that holds a CEK which can just be abbreviated as key therefore key_envelope is accurate?

See f3eceaf for new comments to try to make things more clear.

@quexten quexten left a comment

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.

Looks good. I have one nit on the naming in the serialization format. I would at least add a comment that the key envelope contains the CEK of the data envelope., along with a claude generated diagram, similar to what we have at the top of the invite crypto file, that describes which key protects what.

@JaredSnider-Bitwarden
JaredSnider-Bitwarden merged commit e7c88b8 into main Jul 29, 2026
94 checks passed
@JaredSnider-Bitwarden
JaredSnider-Bitwarden deleted the auth/pm-40520/registration-open-org-invite-seal-unseal branch July 29, 2026 15:20
bw-ghapp Bot added a commit to bitwarden/sdk-swift that referenced this pull request Jul 29, 2026
JaredSnider-Bitwarden added a commit that referenced this pull request Jul 30, 2026
…- Fast Follow (#1309)

## 🎟️ Tracking
<!-- Paste the link to the Jira or GitHub issue or otherwise describe /
point to where this change is coming from. -->
https://bitwarden.atlassian.net/browse/PM-40520
Follow up from #1295

## 📔 Objective
<!-- Describe what the purpose of this PR is, for example what bug
you're fixing or new feature you're adding. -->
To clean up some small remaining items on the new, simple API on the
Auth registration client to seal and unseal the new open organization
invite link data as requested by @quexten
#1295 (review)

---------

Co-authored-by: Dave <3836813+enmande@users.noreply.github.com>
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.

5 participants