Skip to content
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

Adding new device factor source #380

Merged
merged 33 commits into from
Feb 21, 2025
Merged

Conversation

sergiupuhalschi-rdx
Copy link
Contributor

@sergiupuhalschi-rdx sergiupuhalschi-rdx commented Feb 12, 2025

  • Defined a uniffi::Object called DeviceMnemonicBuilder exposing functionality required for the new UI flow for adding a device factor source. Similarly,PasswordMnemonicBuilder and OffDeviceMnemonicBuilder will follow;
  • Added sargonOS.is_factor_source_already_in_use checking if a factor source is already is use;
  • Added sargonOS.add_new_factor_source, which performs the required validations, pre-derives and fills the cache with instances, save factor source into profile, and emits corresponding events.

@sergiupuhalschi-rdx sergiupuhalschi-rdx self-assigned this Feb 12, 2025
@sergiupuhalschi-rdx sergiupuhalschi-rdx changed the title Adding new factor source Adding new device factor source Feb 12, 2025
@sergiupuhalschi-rdx sergiupuhalschi-rdx changed the title Adding new device factor source WIP - Adding new device factor source Feb 12, 2025
Copy link

codecov bot commented Feb 12, 2025

Codecov Report

Attention: Patch coverage is 89.93289% with 15 lines in your changes missing coverage. Please review.

Project coverage is 92.56%. Comparing base (4576ccc) to head (7e9c279).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...on_os/add_factor_source/device_mnemonic_builder.rs 0.00% 6 Missing ⚠️
crates/factors/factors/src/factor_source.rs 84.00% 4 Missing ⚠️
.../uniffi_SPLIT_ME/src/core/utils/builder_arc_map.rs 0.00% 3 Missing ⚠️
...add_factor_source/sargon_os_factor_source_adder.rs 94.59% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #380      +/-   ##
==========================================
- Coverage   92.56%   92.56%   -0.01%     
==========================================
  Files        1227     1230       +3     
  Lines       28348    28495     +147     
  Branches       85       85              
==========================================
+ Hits        26239    26375     +136     
- Misses       2098     2109      +11     
  Partials       11       11              
Flag Coverage Δ
kotlin 98.57% <ø> (ø)
rust 92.20% <89.93%> (+<0.01%) ⬆️
swift 92.67% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

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

let mnemonic_with_passphrase =
MnemonicWithPassphrase::new(mnemonic.clone());
let factor_source = DeviceFactorSource::babylon(
false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you decl_bool_type for this bool? Is it if it is Main?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This cascades into many changes and I'm not sure it's worth it, WDYT?

let mut csprng = OsRng;
let mut confirmation_indices: HashSet<u8> = HashSet::new();

while (confirmation_indices.len() as u8)
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of this you can use:

generate_bytes::<3>().into_iter().map(|b| b as usize).collect::<IndexSet<_>>()

And done.

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 will uncover the case when the same value is generated, with the while we're sure we'll end up with 3 distinct values.

Copy link
Contributor

Choose a reason for hiding this comment

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

right, so lets do generate_bytes::<100>().into_iter().map(|b| b as usize).take(3).collect::<IndexSet<_>>() that will statistically never fail.

Copy link
Contributor

Choose a reason for hiding this comment

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

and when #380 (comment)

it becomes:

generate_bytes::<100>()
    .into_iter()
    .map(|b| b as usize)
    .take(Self::NUMBER_OF_WORDS_OF_MNEMONIC_USER_NEED_TO_CONFIRM_EXCL_CHECKSUM)
    .collect::<IndexSet<_>>()

Copy link
Contributor Author

@sergiupuhalschi-rdx sergiupuhalschi-rdx Feb 17, 2025

Choose a reason for hiding this comment

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

I'm not sure this is a good idea anymore, because I got the test failing a few times, then I increased the value from 100 to 1000 and it seemed fine for a while, but just now it failed again. @CyonAlexRDX Do you agree to revert to the previous code to avoid the tests occasionally failing? Or should we increase the value again? Not sure what's better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Improved this by taking only the first N unique values from the randomly generated ones, so now statistically it shouldn't ever fail.

.await?;

if let Err(e) = self
.pre_derive_and_fill_cache_with_instances_for_factor_source(
Copy link
Contributor

Choose a reason for hiding this comment

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

hmm would be nice with .inspect_err on the Result from pre_derive_and_fill_cache_with_instances_for_factor_source to avoid the if let Err but I think maybe tricky due to async...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would have been nice, but I didn't manage to make it work. Besides await is only allowed inside async functions and blocks error there is also the fact that the operation we perform in case of error returns a Result which we want to propagate.

@sergiupuhalschi-rdx sergiupuhalschi-rdx changed the title WIP - Adding new device factor source Adding new device factor source Feb 17, 2025
@sergiupuhalschi-rdx sergiupuhalschi-rdx marked this pull request as ready for review February 18, 2025 08:44
#[uniffi::export]
impl DeviceMnemonicBuilder {
/// Generates a new mnemonic
pub fn create_new_mnemonic_with_passphrase(self: Arc<Self>) -> Arc<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be named:

generate_new not create_new I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What about create_mnemonic_with_passphrase_from_words? Should it be generate_mnemonic_with_passphrase_from_words for consistency?

Copy link
Contributor

Choose a reason for hiding this comment

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

Generate if generating random entropy. Create If using mnemonic words

Copy link
Contributor

Choose a reason for hiding this comment

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

So your last commit looks good


/// The result of the `build` function from `DeviceMnemonicBuilder`.
#[derive(Debug, PartialEq, uniffi::Enum)]
pub enum DeviceMnemonicBuildResult {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would call this DeviceMnemonicBuildOutcome - we try to always let **Result be a Result type.

#[uniffi::export]
impl DeviceMnemonicBuilder {
/// Generates a new mnemonic
pub fn generate_new_mnemonic_with_passphrase(self: Arc<Self>) -> Arc<Self> {
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we should drop the _with_passphrase here and in create_mnemonic_with_passphrase_from_words since we do not in fact support BIP39Passphrases to be used.


let indices = sut.clone().get_indices_in_mnemonic_of_words_to_confirm();

let r0 = sut.clone().build(
Copy link
Contributor

Choose a reason for hiding this comment

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

r stands for? ah result? lets call it let outcome. And Rust supports shadowing! So no need for outcome1 you can just let outcome = // second and again and again. since it looks like you dont need r0 and r1?

Copy link
Contributor

@CyonAlexRDX CyonAlexRDX left a comment

Choose a reason for hiding this comment

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

LGTM, left some last minor remarks. Good job!

account-for-display@1.2.2
addresses@1.2.2
assert-json@1.2.2
build-info@1.2.2
bytes@1.2.2
cap26-models@1.2.2
clients@1.2.2
core-collections@1.2.2
core-misc@1.2.2
core-utils@1.2.2
drivers@1.2.2
ecc@1.2.2
encryption@1.2.2
entity-by-address@1.2.2
entity-foundation@1.2.2
error@1.2.2
factor-instances-provider@1.2.2
factors@1.2.2
factors-supporting-types@1.2.2
gateway-client-and-api@1.2.2
gateway-models@1.2.2
has-sample-values@1.2.2
hash@1.2.2
hierarchical-deterministic@1.2.2
home-cards@1.2.2
host-info@1.2.2
http-client@1.2.2
identified-vec-of@1.2.2
interactors@1.2.2
key-derivation-traits@1.2.2
keys-collector@1.2.2
manifests@1.2.2
metadata@1.2.2
network@1.2.2
next-derivation-index-ephemeral@1.2.2
numeric@1.2.2
prelude@1.2.2
profile@1.2.2
profile-account@1.2.2
profile-account-or-persona@1.2.2
profile-app-preferences@1.2.2
profile-base-entity@1.2.2
profile-gateway@1.2.2
profile-logic@1.2.2
profile-persona@1.2.2
profile-persona-data@1.2.2
profile-security-structures@1.2.2
profile-state-holder@1.2.2
profile-supporting-types@1.2.2
radix-connect@1.2.2
radix-connect-models@1.2.2
sargon@1.2.2
sargon-os@1.2.2
sargon-os-accounts@1.2.2
sargon-os-derive-public-keys@1.2.2
sargon-os-factors@1.2.2
sargon-os-security-center@1.2.2
sargon-os-signing@1.2.2
sargon-os-transaction@1.2.2
sargon-uniffi@1.2.2
sargon-uniffi-conversion-macros@1.2.2
security-center@1.2.2
short-string@1.2.2
signatures-collector@1.2.2
signing-traits@1.2.2
sub-systems@1.2.2
time-utils@1.2.2
transaction-foundation@1.2.2
transaction-models@1.2.2

Generated by cargo-workspaces
@@ -155,6 +156,63 @@ impl FactorSource {
}
}

impl FactorSource {
pub fn with_details(
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is mostly constrained by mnemonic_with_passphrase, so I would add an its name the mention of MWP, to know this applies only to the kinds of factor sources which have their id derived from MWP.

/// Exposes functions to be called by hosts to be able to use the resulting `MnemonicWithPassphrase`.
#[derive(Debug)]
pub struct DeviceMnemonicBuilder {
mnemonic_with_passphrase: RwLock<Option<MnemonicWithPassphrase>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually need the RwLock here? Is it expected to have many concurrent readers/writers

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 don't think so, at least not according to the current requirements, but I think this is a good thing to have in place for the values accessed/modified by methods called from hosts directly. Do you think we should remove the lock?

Copy link
Contributor

@GhenadieVP GhenadieVP Feb 20, 2025

Choose a reason for hiding this comment

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

Well it is no big impact having it here, it just adds additional complexity for no apparent need. It creates a sort of wrong expectation, that the state really needs to be protected. I would remove it if possible, unless it is really needed.

///
/// And also emits `Event::ProfileSaved` after having successfully written the JSON
/// of the active profile to secure storage.
async fn add_new_factor_source(
Copy link
Contributor

Choose a reason for hiding this comment

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

The name should probably me more specific to mnemonic_with_passphrase

Comment on lines 68 to 75
if factor_source_kind == FactorSourceKind::Device {
self.secure_storage
.save_mnemonic_with_passphrase(
&mnemonic_with_passphrase,
&factor_source.id_from_hash(),
)
.await?;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If this fails, the profile saved factor source should be reverted

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Or we could update the profile after saving the mnemonic to storage.

Comment on lines 88 to 90
self.secure_storage
.delete_mnemonic(&factor_source.id_from_hash())
.await?;
Copy link
Contributor

Choose a reason for hiding this comment

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

What if the kind is not device?

let result = os
.with_timeout(|x| {
x.add_new_factor_source(
FactorSourceKind::Device,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do add a test with another kind than Device.

/// Exposes functions to be called by hosts to be able to use the resulting `MnemonicWithPassphrase`.
#[derive(Debug)]
pub struct DeviceMnemonicBuilder {
mnemonic_with_passphrase: RwLock<Option<MnemonicWithPassphrase>>,
Copy link
Contributor

@GhenadieVP GhenadieVP Feb 20, 2025

Choose a reason for hiding this comment

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

Well it is no big impact having it here, it just adds additional complexity for no apparent need. It creates a sort of wrong expectation, that the state really needs to be protected. I would remove it if possible, unless it is really needed.

Comment on lines 71 to 75
self.update_profile_with(|p| {
p.factor_sources.append(factor_source.clone());
Ok(())
})
.await?;
Copy link
Contributor

@GhenadieVP GhenadieVP Feb 20, 2025

Choose a reason for hiding this comment

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

Same thing applies here - if profile update failed, the previously added device factor source should be removed from the secure storage.
Consider using a block to group together this and self.pre_derive_and_fill_cache_with_instances_for_factor_source, something like:

let setup_result = {
    self.update_profile_with(|p| {
            p.factor_sources.append(factor_source.clone());
            Ok(())
        })
        .await?;
        
    self
        .pre_derive_and_fill_cache_with_instances_for_factor_source(
            factor_source.clone(),
        )
        .await
};

if let Err(e) = setup_result {
   // revert logic on error
}

@sergiupuhalschi-rdx sergiupuhalschi-rdx merged commit 3e4786f into main Feb 21, 2025
12 of 13 checks passed
@sergiupuhalschi-rdx sergiupuhalschi-rdx deleted the sp/add-new-factor-builder branch February 21, 2025 09:31
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.

5 participants