Skip to content

Conversation

iinuwa
Copy link

@iinuwa iinuwa commented Oct 1, 2025

🎟️ Tracking

PM-26177

📔 Objective

In order to set up PRF passkeys on mobile clients, we need to create a rotateable key set. This adds an internal method to the SDK that can be used by a future PRF derivation method.

⏰ 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

@iinuwa iinuwa requested review from a team as code owners October 1, 2025 15:23
@iinuwa iinuwa requested review from coroiu and quexten October 1, 2025 15:23
@iinuwa iinuwa changed the base branch from main to km/PM-26354/asymmetric-keystore-wrapping October 1, 2025 15:25
Copy link
Contributor

github-actions bot commented Oct 1, 2025

Logo
Checkmarx One – Scan Summary & Detailsad6e7d97-30af-407f-8f10-a3ca02369ea1

Great job! No new security vulnerabilities introduced in this pull request

Copy link

sonarqubecloud bot commented Oct 1, 2025

Copy link

codecov bot commented Oct 1, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.39%. Comparing base (0984ec4) to head (e74f17c).

Additional details and impacted files
@@                             Coverage Diff                              @@
##           km/PM-26354/asymmetric-keystore-wrapping     #486      +/-   ##
============================================================================
+ Coverage                                     78.25%   78.39%   +0.13%     
============================================================================
  Files                                           283      283              
  Lines                                         27713    27800      +87     
============================================================================
+ Hits                                          21688    21793     +105     
+ Misses                                         6025     6007      -18     

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

@coroiu
Copy link
Contributor

coroiu commented Oct 2, 2025

I'll defer my review to @quexten. You are introducing a "next version" of these sets in the form of envelopes, right? Did you have a migration plan for the key sets or are they going to have to exist together for a while?

@quexten
Copy link
Contributor

quexten commented Oct 2, 2025

You are introducing a "next version" of these sets in the form of envelopes, right?

No, a re-design of RotateableKeySets is not currently in-progress, and they are a supported API. If we do re-design them, then that's a separate thing that's not blocking here.

#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
pub struct RotateableKeySet {
Copy link
Contributor

Choose a reason for hiding this comment

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

A thought on the API (open for discussion, as pseudo code):

  1. Move RotateableKeySet to crypto crate, then:
impl RotateableKeySet<...> {
  pub fn new(externalKey: SymmetricCryptoKey, key_slot_to_wrap: KeyId) -> Self;
  pub fn unlock(externalKey: SymmetricCryptoKey, key_slot_to_unwrap_to: KeyId) -> Result<KeyId, ...>;
}

That way we would not need to make the functions from the first PR in the set public, and also allow arbitrary keys to be wrapped, not just the user key.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, I agree that's more functional.

Some questions:

  • These methods would need to take &KeyStoreContext (or moved to impl KeyStoreContext and take &self) in order for the KeyId to mean anything, right?
  • Do we prefer the term "external key" over "wrapping key" in this context?
  • Does it make sense to be generic over KeyId? Can we provide the same guarantees with asymmetric keys?
e, d = en-/decapsulation key pair
s = symmetric external key
k = key to wrap (symmetric key, or private key)

KeySet = {
  SymmetricWrap(s, d),
  Wrap(k, e),  // symmetric or asymmetric, depending on type of `k`
  Encaps(e, k),
}

In order for Wrap(k, e) to not be tampered by the server, Wrap must provide integrity and authentication, which RSA alone does not, right?


To make sure I understand, and taking in input from the other PRs, we'd want to go with something like (just roughed out):

// caller wrap
let store = client.internal.get_key_store();
let ctx = store.context();
let external_key_id = SymmetricKeyId::Local("external_key");
ctx.derive_symmetric_key_from_prf(prf, external_key_id)?;

let keyset = ctx.create_rotateable_key_set(external_key_id, SymmetricKeyId::User)?;
// or
// let keyset = RotateableKeySet::new(&ctx, external_key_id, SymmetricKeyId::User)?;
// caller unlock
let keyset: RotateableKeySet = ...;
let store = client.internal.get_key_store();
let ctx = store.context();

let external_key_id = SymmetricKeyId::Local("external_key");
ctx.derive_symmetric_key_from_prf(prf, external_key_id)?;

let key_to_unwrap = SymmetricKeyId::Local("key_to_unwrap");
ctx.unlock_rotateable_key_set(external_key_id, keyset, key_to_unwrap);
// or
// keyset.unlock(&ctx, external_key_id, key_to_unwrap);
// implementation

impl KeyStoreContext<...> {
    fn create_rotateable_key_set(external_key_id: SymmetricKeyId, key_to_wrap: KeyId) -> Result<RotateableKeySet> {
        // use ephemeral keys to create key set, don't store in context as key IDs
        let (encapsulation_key, decapsulation_key) = rsa::make_key_pair()
        let wrapped_decapsulation_key = self.encrypt_data_with_symmetric_key(...)
        let encapsulated_key = UnsignedSharedKey::encapsulate_key_unsigned(self.get_key(key_to_wrap));
        // I only how to do this for symmetric keys, not sure about asymmetric
        let wrapped_encapsulation_key = self.encrypt_data_with_symmetric_key(key_to_wrap, encapsulation_key);
        return RotateableKeySet {
            // ...
        }
    }
}

// encapsulate user key
let encapsulation_key_id = AsymmetricKeyId::Local("encapsulation_key");
ctx.make_asymmetric_key(encapsulation_key_id)?;
let encapsulated_user_key =
Copy link
Contributor

Choose a reason for hiding this comment

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

To me, this is unintutive behavior. At least the key_to_wrap should be exposed as a key id parameter here?

Copy link
Author

Choose a reason for hiding this comment

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

yes, that would make more sense. I can change that and update the documentation.

@iinuwa
Copy link
Author

iinuwa commented Oct 2, 2025

Converting to draft pending further discussion with the KM team.

@iinuwa iinuwa marked this pull request as draft October 2, 2025 14:27
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.

3 participants