Skip to content

[Innovation-Sprint/PM-19065] Add opaque authentication / unlock #185

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from

Conversation

quexten
Copy link
Contributor

@quexten quexten commented Mar 11, 2025

🎟️ Tracking

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

📔 Objective

Adds functions to use OPAQUE-key exchange from the clients. Additionally, adds rotateable keyset to the SDK.

Tests are provided against a local "mockserver" implementation of the server.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation
    team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link

codecov bot commented Mar 11, 2025

Codecov Report

Attention: Patch coverage is 69.73180% with 158 lines in your changes missing coverage. Please review.

Project coverage is 66.40%. Comparing base (ae3c08d) to head (6155e1d).

Files with missing lines Patch % Lines
crates/bitwarden-core/src/mobile/crypto_client.rs 0.00% 58 Missing ⚠️
crates/bitwarden-wasm-internal/src/crypto.rs 0.00% 57 Missing ⚠️
crates/bitwarden-crypto/src/opaque_ke/opaque.rs 86.64% 37 Missing ⚠️
crates/bitwarden-crypto/src/opaque_ke/error.rs 0.00% 3 Missing ⚠️
crates/bitwarden-crypto/src/rotateable_keyset.rs 92.50% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #185      +/-   ##
==========================================
+ Coverage   66.31%   66.40%   +0.08%     
==========================================
  Files         199      204       +5     
  Lines       15609    16130     +521     
==========================================
+ Hits        10351    10711     +360     
- Misses       5258     5419     +161     

☔ 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.

Copy link
Contributor

github-actions bot commented Mar 11, 2025

Logo
Checkmarx One – Scan Summary & Detailsc63a7c2e-f611-4de7-b6b1-e3a90ccfaed1

Great job, no security vulnerabilities found in this Pull Request

@quexten quexten changed the title [Innovation-Sprint/PoC] Add opaque [Innovation-Sprint/PoC] Add opaque authentication / unlock Mar 11, 2025
@quexten quexten marked this pull request as ready for review March 21, 2025 13:53
@quexten quexten changed the title [Innovation-Sprint/PoC] Add opaque authentication / unlock [Innovation-Sprint/PM-19065] Add opaque authentication / unlock Mar 21, 2025
@quexten quexten requested a review from a team as a code owner March 25, 2025 13:17
@quexten quexten requested a review from dani-garcia March 25, 2025 13:17
Copy link
Member

@justindbaur justindbaur left a comment

Choose a reason for hiding this comment

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

Just a few questions.

password: &str,
) -> Result<types::ClientRegistrationStartResult, Error> {
let result =
ClientRegistration::<Self>::start(&mut rand::thread_rng(), password.as_bytes())?;
Copy link
Member

Choose a reason for hiding this comment

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

rand::thread_rng() is correct? Most examples I had seen were using opaque_ke::rand::rngs::OsRng.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is OK because:

  • We use thread_rng everywhere in the SDK, we don't use OsRng
  • ThreadRng implements the CryptoRng marker trait, and is seeded via OsRng
  • opaque-ke expects a CryptoRng + RngCore, which both OsRng but also ThreadRng fulfill.

I think the main difference to OsRng is speed, but I'll leave this open to @dani-garcia to comment on.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, OsRng is reading directly from the OS entropy source, while ThreadRng just seeds a PRNG with the OS entropy.

The rand docs qualify it as ... good compromise between cryptographic security, fast generation ... https://github.com/rust-random/rand/blob/master/SECURITY.md#specific-generators

}

#[cfg(test)]
mod test {
Copy link
Member

Choose a reason for hiding this comment

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

Should we add any unhappy path tests? My thinking is the error that we return on a
wrong password should be well documented and tested so that we can handle that on the client side and show a nice error.

/// The client side, serialized state of the registration process.
/// MUST NOT BE SHARED WITH THE SERVER and must be cleared from memory after registration
/// finish.
pub state: ByteBuf,
Copy link
Member

Choose a reason for hiding this comment

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

Just curious, what does ByteBuf become on the TS side? Is it special cased to become ArrayBuffer, UInt8Array, or does it copy the layout of this type? Is it actually easy to clear from memory?

Copy link
Contributor Author

@quexten quexten Mar 25, 2025

Choose a reason for hiding this comment

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

https://github.com/AmbientRun/tsify-next/blob/2e5e0a174fb4dd3b87bddec93286250001f57c46/tsify-macros/src/typescript.rs#L363

As far as I understand, this is the only way to get Uint8array in TS directly in tsify-next. Otherwise you get number[], which has to be manually converted again.

Copy link
Member

Choose a reason for hiding this comment

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

We generally encode these using b64 in other places. @dani-garcia not sure if that is more appropriate or if we revisit our practice. (I believe we b64 encode a few things for mobile at least)

Copy link
Member

Choose a reason for hiding this comment

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

Yeah we've had complaints in the swift native apps when using bytes as they were hard to work with, something about equality not being implemented for the swift bytes type I think.

That said, using b64 is probably just as annoying and pushing the conversion work to the clients. I wonder if this just means we need to consider having our own Bytes type that does the right conversion for each platform.

Copy link
Member

@Hinton Hinton left a comment

Choose a reason for hiding this comment

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

I assume this initiative has been green lit and will move forward hence the requests for reviews. And the code should be considered "production ready".

I don't have any knowledge of the OPAQUE protocol so this review is purely from a rust and SDK perspective.


The pull request description is fairly lightweight and contains little information about what this work entails. Could we expand it a bit or at least link to the appropriate confluence documents.

As far as I can tell this has little to do with cryptographic primitives and is more a new registration / login flow. If that's accurate we should consider lifting it out of the crypto crate which would also simplify ownership if we suspect everything to not be owned by KM.

There are a lot of public types and fields in this PR. Do all of them need pub visibility or can we restrict it? It's always easier to make more things public than to decrease visibility in the future.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

question: There seems to be little reason to use the client at all here since it's just obfuscating direct calls to types?

This is related to my comment about CipherConfiguration leaking sensitive values.

use tsify_next::Tsify;

#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
Copy link
Member

Choose a reason for hiding this comment

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

question: This leaks the sensitive values to the presentation layer. Would we prefer that these stay inside the rust world or that we pass a object back and forth?

/// The client side, serialized state of the registration process.
/// MUST NOT BE SHARED WITH THE SERVER and must be cleared from memory after registration
/// finish.
pub state: ByteBuf,
Copy link
Member

Choose a reason for hiding this comment

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

We generally encode these using b64 in other places. @dani-garcia not sure if that is more appropriate or if we revisit our practice. (I believe we b64 encode a few things for mobile at least)

Comment on lines +9 to +12
#[error("Error creating message {0}")]
Message(String),
#[error("Error deserializing message")]
Deserialize,
Copy link
Member

Choose a reason for hiding this comment

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

issue: Unused?

/// be accessed by having access to the upstream key.
#[derive(Deserialize, Serialize)]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct RotateableKeyset {
Copy link
Member

@Hinton Hinton Mar 26, 2025

Choose a reason for hiding this comment

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

issue: Should probably be called RotatableKeyset?

Consider installing code spell checker and resolving any unknown words. And adding any appropriate to the project word list.

encapsulating_key_material: &[u8],
encapsulated_key: &SymmetricCryptoKey,
) -> Result<Self, CryptoError> {
let key = if encapsulating_key_material.len() == 64 {
Copy link
Member

Choose a reason for hiding this comment

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

question: Is there a reason we verify the length here? What are we attempting to protect against? (add a comment?)

I think it's more appropriate to use a guard statement here

if encapsulating_key_material.len() != 64 {
    return Err(CryptoError::InvalidKey);
}
let key = SymmetricCryptoKey::try_from(encapsulating_key_material.to_vec())?;

Comment on lines +132 to +149

/// Create a new rotateable keyset from an export key and an encapsulated key
/// Note: The export key must be 32 bytes
pub fn create_rotateablekeyset_from_exportkey(
&self,
export_key: &mut [u8],
encapsulated_key: &SymmetricCryptoKey,
) -> Result<bitwarden_crypto::rotateable_keyset::RotateableKeyset, CryptoError> {
bitwarden_crypto::rotateable_keyset::RotateableKeyset::new(export_key, encapsulated_key)
}

pub fn decapsulate_key_from_rotateablekeyset(
&self,
rotateable_keyset: bitwarden_crypto::rotateable_keyset::RotateableKeyset,
encapsulating_key: &[u8],
) -> Result<SymmetricCryptoKey, CryptoError> {
rotateable_keyset.decapsulate_key(encapsulating_key)
}
Copy link
Member

Choose a reason for hiding this comment

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

@dani-garcia will these APIs work in the more limited mobile environment? I don't believe mobile can create a SymmetricCryptoKey for example.

Copy link
Member

Choose a reason for hiding this comment

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

The exact API won't, but we can adapt them slightly in bitwarden_uniffi

The SymmetricCryptoKey in create_rotateablekeyset_from_exportkey would be coming from the ClientRegistrationFinishResult struct in the opaque_register_finish function. I imagine in the UniFFI layer we could pass the entire struct as an opaque pointer directly so the mobile apps won't have to deal with the key themselves.

For decapsulate_key_from_rotateablekeyset, the UniFFI function would just set the result key as the user key and return Ok(()), I think

Comment on lines +122 to +125
/// The client side, serialized state of the login process.
/// MUST NOT BE SHARED WITH THE SERVER and must be cleared from memory after registration
/// finish.
pub state: ByteBuf,
Copy link
Member

Choose a reason for hiding this comment

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

Sounds like this state should be encapsulated by the SDK and not leave it's boundary?

/// if the cipher parameters have a larger key size, the key will be truncated.
/// This key is consistent between authentications.
pub export_key: ByteBuf,

Copy link
Member

Choose a reason for hiding this comment

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

nit: Inconsistent use of newline.

@quexten
Copy link
Contributor Author

quexten commented Mar 26, 2025

As far as I can tell this has little to do with cryptographic primitives and is more a new registration / login flow. If that's accurate we should consider lifting it out of the crypto crate which would also simplify ownership if we suspect everything to not be owned by KM.

Cryptographic primitives here refer to the most basic building blocks of Opaque, as exposed to the outside - I.e entire, high level messages (from OPAQUES view). I realize that is leading to some confusion here, so I'll refrain from the word from now on.

@quexten quexten marked this pull request as draft March 28, 2025 13:38
@quexten
Copy link
Contributor Author

quexten commented Mar 28, 2025

Setting back to draft for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants