Skip to content

[PM-20493] Add key wrapping / wrapped key facade for encstring & expose keywrap to purecrypto #221

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 173 commits into
base: main
Choose a base branch
from

Conversation

quexten
Copy link
Contributor

@quexten quexten commented Apr 21, 2025

🎟️ Tracking

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

📔 Objective

Refactors how wrapped keys are represented in the SDK. We now add a facade around them, that only exposes the unwrap method. Additionally, this exposes key-wrapping to wasm via PureCrypto, in order to act as a backwards compatibility layer for all clients code that has not been migrated to use the SDK entirely yet.

Note: This adds a new module - "safe" to the bitwarden-crypto crate. Any of the methods or structs exposed here should be used first and foremost before anything else within the crypto crate.

🦖 System evolution - "Safe module"

The safe module is meant to serve as a repository for developers to draw from first and foremost. We may want to re-consider moving all other unsafer methods (RSA encrypt, direct encrypt of data, pin-key, masterkey) into a module - hazmat - later on and moving safe to the top level, once the safe crate contains sufficient good abstractions.

The abstractions contained here should be simple to understand, easy to reason about, easy to use in combination and very hard to mis-use.

For instance, master-password unlock, and pin unlock could be represented as:

// envelope contains the KDF settings in the encstring serialization
let envelope: PasswordProtectedKeyEnvelope = PasswordProtectedKeyEnvelope::seal(&password, &user_key, &kdf_settings);
let user_key = envelope.unseal(&password);

which completely eliminates the need for developers to care about synchronizing kdf settings and salts, or even caring about things like a masterkey existing.

⏰ 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

@quexten quexten changed the base branch from main to km/cose April 29, 2025 11:45
Base automatically changed from km/cose to main May 6, 2025 15:13
@quexten quexten marked this pull request as draft May 6, 2025 17:50
@@ -43,3 +59,11 @@ impl UniffiCustomTypeConverter for UnsignedSharedKey {
obj.to_string()
}
}

// Uniffi doesn't emit unused types, this is a dummy record to ensure that the custom type
// converters are emitted
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can be removed when #221 is merged.

Copy link

sonarqubecloud bot commented May 7, 2025

@quexten quexten marked this pull request as ready for review May 7, 2025 10:47
@quexten quexten removed the request for review from addisonbeck May 7, 2025 11:50
@quexten quexten marked this pull request as draft May 8, 2025 09:48
quexten added a commit that referenced this pull request May 9, 2025
## 🎟️ Tracking

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

## 📔 Objective

Add remaining missing functions needed to port encrypt service to sdk.
Ref:
https://github.com/bitwarden/clients/blob/87a1f4e8ac928edfb05a283372550d0792c71827/libs/common/src/key-management/crypto/abstractions/encrypt.service.ts#L105-L188

Note: The way that this uses the key context is not ideal, but this
should not be handled in this PR. This PR is to make the public
interface available to unblock clients changes. Follow-up work (such as
#221) can expose better
apis from bitwarden-crypto and move PureCrypto to it.

## ⏰ 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

<!-- Suggested interactions but feel free to use (or not) as you desire!
-->

- 👍 (`:+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
Member

@dani-garcia dani-garcia left a comment

Choose a reason for hiding this comment

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

Looks pretty good to me, just some minor details

/// key_to_wrap.
///
/// Wrapped keys such as cipher keys, or attachment keys, are used to create a layer of indirection,
/// so that keys can be shared mor granularly, and so that data can be rotated more easily.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// so that keys can be shared mor granularly, and so that data can be rotated more easily.
/// so that keys can be shared more granularly, and so that data can be rotated more easily.

Comment on lines +48 to +54
impl std::fmt::Debug for WrappedSymmetricKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WrappedSymmetricKey")
.field("inner_enc_string", &self.as_inner())
.finish()
}
}
Copy link
Member

Choose a reason for hiding this comment

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

In this case we can probably just tag WrappedSymmetricKey with #[derive(Debug)] directly instead of implementing it by hand.

/// * `new_key_id` - The key id where the decrypted key will be stored. If it already exists, it
/// will be overwritten
/// * `encrypted_key` - The key to decrypt
/// * `key_to_unwrap` - The key to decrypt
Copy link
Member

Choose a reason for hiding this comment

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

There's still a reference to decryption instead of unwrapping (The key to decrypt -> The key to unwrap). To avoid the repetition with the comment, we could also rename the parameter to more closely resemble it's type too:

Suggested change
/// * `key_to_unwrap` - The key to decrypt
/// * `wrapped_key` - The key to unwrap

make_user_key(rand::thread_rng(), self)
}

/// Encrypt the users user key
pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result<EncString> {
pub fn encrypt_user_key(&self, user_key: &SymmetricCryptoKey) -> Result<WrappedSymmetricKey> {
Copy link
Member

Choose a reason for hiding this comment

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

Question: Will we want to rename these to also use the wrap/unwrap naming?

@@ -70,12 +70,12 @@ mod tests {

let master_key = MasterKey::derive(password, email, &kdf).unwrap();

let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=";
let user_key: EncString = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

We implement FromStr for WrappedSymmetricKey as well, so if you remove the .into() below you can skip the type here and rust will infer it correctly as a WrappedSymmetricKey:

Suggested change
let user_key: EncString = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();
let user_key = "2.Q/2PhzcC7GdeiMHhWguYAQ==|GpqzVdr0go0ug5cZh1n+uixeBC3oC90CIe0hd/HWA/pTRDZ8ane4fmsEIcuc8eMKUt55Y2q/fbNzsYu41YTZzzsJUSeqVjT8/iTQtgnNdpo=|dwI+uyvZ1h/iZ03VQ+/wrGEFYVewBUUl/syYgjsNMbE=".parse().unwrap();

This also probably applies to the other test cases that were updated in this PR

@@ -110,10 +110,13 @@ impl AuthClient {
password: String,
encrypted_user_key: String,
) -> Result<String> {
let encrypted_user_key = encrypted_user_key
.parse::<EncString>()
Copy link
Member

Choose a reason for hiding this comment

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

We can also parse into WrappedSymmetricKey here and remove the .into()

Suggested change
.parse::<EncString>()
.parse::<WrappedSymmetricKey>()

wrapped_key: String,
wrapping_key: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
let wrapped_key: WrappedSymmetricKey = EncString::from_str(&wrapped_key)?.into();
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
let wrapped_key: WrappedSymmetricKey = EncString::from_str(&wrapped_key)?.into();
let wrapped_key = WrappedSymmetricKey::from_str(&wrapped_key)?;

@@ -453,7 +454,7 @@ impl CipherView {
ctx: &mut KeyStoreContext<KeyIds>,
key: SymmetricKeyId,
) -> Result<(), CryptoError> {
let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key)?;
let old_ciphers_key = Cipher::decrypt_cipher_key(ctx, key, &self.key.take())?;
Copy link
Member

Choose a reason for hiding this comment

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

Now that the keys are wrapped, the take here can be removed.

*attachment_key = dec_attachment_key.encrypt(ctx, new_key)?;
let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key");
ctx.unwrap_symmetric_key(old_key, tmp_attachment_key_id, attachment_key)?;
*attachment_key = ctx.wrap_symmetric_key(new_key, tmp_attachment_key_id)?;
Copy link
Member

Choose a reason for hiding this comment

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

I'm wondering if we should offer a function in the context to do the unwrap/wrap for you, as we're already using it a couple of times here.

@@ -489,8 +490,9 @@ impl CipherView {
if let Some(attachments) = &mut self.attachments {
for attachment in attachments {
if let Some(attachment_key) = &mut attachment.key {
let dec_attachment_key: Vec<u8> = attachment_key.decrypt(ctx, old_key)?;
*attachment_key = dec_attachment_key.encrypt(ctx, new_key)?;
let tmp_attachment_key_id = SymmetricKeyId::Local("attachment_key");
Copy link
Member

Choose a reason for hiding this comment

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

Usually we declare these ids as const, we should probably do the same for consistency.

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.

2 participants