From 5fdde6651d33041622ffadfa1fcb82a4e158c463 Mon Sep 17 00:00:00 2001 From: Alexander Cyon <116169792+CyonAlexRDX@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:38:50 +0100 Subject: [PATCH] Rules for Roles (#286) Implement SecurityShieldBuilder which is UniFFI exported, and all logic needed to power it (rules for roles). --- .pre-commit-config.yaml | 2 +- Cargo.lock | 34 +- ...uctureOfFactorSources+Wrap+Functions.swift | 17 - .../TransactionManifest+Wrap+Functions.swift | 4 +- .../MFA/SecurityShieldBuilder+Swifified.swift | 45 + .../TestCases/Prelude/Decimal192Tests.swift | 6 +- .../MFA/SecurityShieldsBuilderTests.swift | 252 +++ ...ecurityStructureOfFactorSourcesTests.swift | 11 - .../RET/TransactionManifestTests.swift | 2 +- crates/sargon-uniffi/Cargo.toml | 17 +- .../decl_security_structure_of.rs | 155 -- .../matrices/decl_matrix_macro.rs | 88 + .../matrices/matrix_of_factor_instances.rs | 8 + .../matrices/matrix_of_factor_source_ids.rs | 39 + .../matrices/matrix_of_factor_sources.rs | 27 + .../mfa/security_structures/matrices/mod.rs | 10 + .../profile/mfa/security_structures/mod.rs | 21 +- ...ource_in_role_builder_validation_status.rs | 58 + .../mfa/security_structures/models/mod.rs | 5 + .../security_structures/models}/role_kind.rs | 5 +- .../roles/decl_role_macro.rs | 194 ++ .../mfa/security_structures/roles/mod.rs | 10 + .../roles/roles_factor_instances.rs | 8 + .../roles/roles_factor_source_ids.rs | 8 + .../roles/roles_factor_sources.rs | 8 + .../security_shield_builder.rs | 679 ++++++ .../security_shield_builder_invalid_reason.rs | 77 + .../security_shield_prerequisites_status.rs | 19 + .../security_structure_metadata.rs | 5 + ...security_structure_of_factor_source_ids.rs | 25 - .../security_structure_of_factor_sources.rs | 46 - .../security_structures/mod.rs | 7 + .../security_structure_of_factor_instances.rs | 8 +- ...security_structure_of_factor_source_ids.rs | 40 + .../security_structure_of_factor_sources.rs | 27 + .../factor_instance/badge_virtual_source.rs | 5 + .../factor_instance/factor_instance_badge.rs | 2 + .../src/profile/v100/factors/factor_source.rs | 2 + crates/sargon-uniffi/src/signing/mod.rs | 2 - .../sargon_os_security_structures.rs | 14 + crates/sargon/Cargo.toml | 2 +- .../factor_instances_provider_unit_tests.rs | 682 +++--- ...curify_entity_factor_instances_provider.rs | 63 +- .../cap26/paths/account_path.rs | 2 +- .../cap26/paths/identity_path.rs | 2 +- crates/sargon/src/lib.rs | 4 +- .../account/query_security_structures.rs | 121 + .../decl_security_structure_of.rs | 378 ---- ...confirmation_role_with_factor_instances.rs | 48 - .../matrix_of_factor_instances.rs | 202 -- .../primary_role_with_factor_instances.rs | 63 - .../recovery_role_with_factor_instances.rs | 49 - .../security_structure_of_factor_instances.rs | 75 - ...onfirmation_role_with_factor_source_ids.rs | 52 - .../matrix_of_factor_source_ids.rs | 50 - .../factor_source_id_level/mod.rs | 15 - .../primary_role_with_factor_source_ids.rs | 49 - .../recovery_role_with_factor_source_ids.rs | 46 - ...security_structure_of_factor_source_ids.rs | 56 - ...ecurity_structures_of_factor_source_ids.rs | 19 - .../matrix_of_factor_sources.rs | 39 - .../primary_role_with_factor_sources.rs | 47 - .../security_structure_of_factor_sources.rs | 213 -- .../security_structures_of_factor_sources.rs | 19 - .../abstract_matrix_builder_or_built.rs | 112 + .../matrices/builder/error.rs | 64 + .../matrices/builder/matrix_builder.rs | 406 ++++ .../builder/matrix_builder_unit_tests.rs | 1994 +++++++++++++++++ .../matrices/builder/matrix_template.rs | 550 +++++ .../matrices/builder/mod.rs | 9 + .../matrices/factor_source_id_samples.rs | 72 + .../matrices/matrix_of_factor_instances.rs | 406 ++++ .../matrices/matrix_of_factor_source_ids.rs | 381 ++++ .../matrices/matrix_of_factor_sources.rs | 72 + .../mfa/security_structures/matrices/mod.rs | 14 + .../profile/mfa/security_structures/mod.rs | 24 +- .../security_structures/role_with_factors.rs | 18 - .../roles/abstract_role_builder_or_built.rs | 216 ++ .../confirmation_roles_builder_unit_tests.rs | 345 +++ .../security_structures/roles/builder/mod.rs | 7 + .../primary_roles_builder_unit_tests.rs | 965 ++++++++ .../recovery_roles_builder_unit_tests.rs | 421 ++++ .../roles/builder/roles_builder.rs | 882 ++++++++ .../roles/builder/roles_builder_unit_tests.rs | 88 + ...confirmation_role_with_factor_instances.rs | 33 + ...archical_deterministic_factor_instances.rs | 295 +++ .../factor_instance_level/mod.rs | 8 +- .../primary_role_with_factor_instances.rs | 111 + .../recovery_role_with_factor_instances.rs | 33 + .../role_with_factor_instances.rs | 56 + ...onfirmation_role_with_factor_source_ids.rs | 104 + .../factor_source_id_level/mod.rs | 9 + .../primary_role_with_factor_source_ids.rs | 107 + .../recovery_role_with_factor_source_ids.rs | 66 + .../roles_with_factor_ids.rs | 4 + .../factor_source_kind_level/mod.rs | 3 + .../factor_source_kind_level/role_template.rs | 105 + .../confirmation_role_with_factor_sources.rs | 27 +- .../factor_levels}/factor_source_level/mod.rs | 7 +- .../primary_role_with_factor_sources.rs | 36 + .../recovery_role_with_factor_sources.rs | 25 +- .../roles_with_factor_sources.rs | 67 + .../roles/factor_levels/mod.rs | 9 + .../mfa/security_structures/roles/mod.rs | 7 + .../security_shield_builder.rs | 985 ++++++++ .../security_shield_builder_invalid_reason.rs | 248 ++ .../security_shield_prerequisites_status.rs | 16 + .../security_structure_id.rs | 0 .../abstract_security_structure_of_factors.rs | 47 + .../security_structure_of_factors/mod.rs | 7 + ...security_structure_of_factor_source_ids.rs | 218 ++ .../security_structure_of_factor_sources.rs | 122 + .../profile/v100/app_preferences/security.rs | 134 +- .../entity_security_state.rs | 79 +- .../v100/factors/factor_source_category.rs | 17 + .../v100/factors/factor_source_kind.rs | 37 + .../profile/v100/factors/is_factor_source.rs | 4 + crates/sargon/src/profile/v100/factors/mod.rs | 2 + .../signing/collector/signatures_collector.rs | 17 +- .../general_role_with_hd_factor_instance.rs | 225 -- .../sargon/src/signing/petition_types/mod.rs | 2 - .../petition_types/petition_for_entity.rs | 45 +- .../sargon_os_security_structures.rs | 22 + .../src/types/samples/account_samples.rs | 46 +- .../src/types/samples/persona_samples.rs | 47 +- crates/sargon/tests/vectors/main.rs | 2 +- .../os/storage/KeystoreAccessRequestTest.kt | 48 +- 127 files changed, 12140 insertions(+), 2676 deletions(-) create mode 100644 apple/Sources/Sargon/Extensions/Swiftified/Profile/MFA/SecurityShieldBuilder+Swifified.swift create mode 100644 apple/Tests/TestCases/Profile/MFA/SecurityShieldsBuilderTests.swift delete mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/decl_security_structure_of.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/decl_matrix_macro.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/mod.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/models/factor_source_in_role_builder_validation_status.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/models/mod.rs rename crates/sargon-uniffi/src/{signing => profile/mfa/security_structures/models}/role_kind.rs (66%) create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/roles/decl_role_macro.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/roles/mod.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_instances.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_source_ids.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_sources.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs delete mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs delete mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/mod.rs rename crates/sargon-uniffi/src/profile/mfa/security_structures/{ => security_structures}/security_structure_of_factor_instances.rs (76%) create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_source_ids.rs create mode 100644 crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_sources.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_instance_level/confirmation_role_with_factor_instances.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_instance_level/matrix_of_factor_instances.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_instance_level/primary_role_with_factor_instances.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_instance_level/recovery_role_with_factor_instances.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_instance_level/security_structure_of_factor_instances.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/confirmation_role_with_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/matrix_of_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/mod.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/primary_role_with_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/recovery_role_with_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_of_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structures_of_factor_source_ids.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_level/matrix_of_factor_sources.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_level/primary_role_with_factor_sources.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structure_of_factor_sources.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structures_of_factor_sources.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/abstract_matrix_builder_or_built.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/builder/error.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder_unit_tests.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_template.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/builder/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/factor_source_id_samples.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/matrices/mod.rs delete mode 100644 crates/sargon/src/profile/mfa/security_structures/role_with_factors.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/abstract_role_builder_or_built.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/confirmation_roles_builder_unit_tests.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/primary_roles_builder_unit_tests.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/recovery_roles_builder_unit_tests.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder_unit_tests.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs rename crates/sargon/src/profile/mfa/security_structures/{ => roles/factor_levels}/factor_instance_level/mod.rs (56%) create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/role_template.rs rename crates/sargon/src/profile/mfa/security_structures/{ => roles/factor_levels}/factor_source_level/confirmation_role_with_factor_sources.rs (54%) rename crates/sargon/src/profile/mfa/security_structures/{ => roles/factor_levels}/factor_source_level/mod.rs (55%) create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs rename crates/sargon/src/profile/mfa/security_structures/{ => roles/factor_levels}/factor_source_level/recovery_role_with_factor_sources.rs (54%) create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/roles/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_shield_builder.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs rename crates/sargon/src/profile/mfa/security_structures/{factor_source_id_level => }/security_structure_id.rs (100%) create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/abstract_security_structure_of_factors.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/mod.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_source_ids.rs create mode 100644 crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_sources.rs create mode 100644 crates/sargon/src/profile/v100/factors/factor_source_category.rs delete mode 100644 crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de9d3a614..d49c99393 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_install_hook_types: [pre-push] default_stages: [pre-push] repos: - repo: https://github.com/crate-ci/typos - rev: v1.22.7 + rev: v1.28.1 hooks: - id: typos - repo: local diff --git a/Cargo.lock b/Cargo.lock index 3586fa41d..c5d80a175 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2193,6 +2193,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "preinterpret" +version = "0.2.0" +source = "git+https://github.com/dhedey/preinterpret?rev=6754b92bdead0ddd6f69fbee7d782180d6351605#6754b92bdead0ddd6f69fbee7d782180d6351605" +dependencies = [ + "proc-macro2", + "syn 2.0.85", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -2763,7 +2772,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.73" +version = "1.1.74" dependencies = [ "actix-rt", "aes-gcm", @@ -2818,7 +2827,7 @@ dependencies = [ [[package]] name = "sargon-uniffi" -version = "1.1.73" +version = "1.1.74" dependencies = [ "actix-rt", "assert-json-diff", @@ -2836,6 +2845,7 @@ dependencies = [ "itertools 0.12.0", "log", "paste 1.0.14", + "preinterpret", "pretty_assertions", "pretty_env_logger", "radix-engine-toolkit", @@ -3689,7 +3699,7 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uniffi" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "camino 1.1.9", @@ -3704,7 +3714,7 @@ dependencies = [ [[package]] name = "uniffi_bindgen" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "camino 1.1.9", @@ -3727,7 +3737,7 @@ dependencies = [ [[package]] name = "uniffi_build" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "camino 1.1.9", @@ -3737,7 +3747,7 @@ dependencies = [ [[package]] name = "uniffi_checksum_derive" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "quote", "syn 2.0.85", @@ -3746,7 +3756,7 @@ dependencies = [ [[package]] name = "uniffi_core" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "bytes", @@ -3758,7 +3768,7 @@ dependencies = [ [[package]] name = "uniffi_macros" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "bincode", "camino 1.1.9", @@ -3775,7 +3785,7 @@ dependencies = [ [[package]] name = "uniffi_meta" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "bytes", @@ -3786,7 +3796,7 @@ dependencies = [ [[package]] name = "uniffi_testing" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "camino 1.1.9", @@ -3798,7 +3808,7 @@ dependencies = [ [[package]] name = "uniffi_udl" version = "0.28.3" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "anyhow", "textwrap", @@ -4064,7 +4074,7 @@ dependencies = [ [[package]] name = "weedle2" version = "5.0.0" -source = "git+https://github.com/mozilla/uniffi-rs/?rev=2c003b16d1e70e1914b5d8ceb517eef3676cd187#2c003b16d1e70e1914b5d8ceb517eef3676cd187" +source = "git+https://github.com/sajjon/uniffi-rs/?rev=fe40d992b32103876112713cb5e8303b54647183#fe40d992b32103876112713cb5e8303b54647183" dependencies = [ "nom", ] diff --git a/apple/Sources/Sargon/Extensions/Methods/Profile/MFA/SecurityStructureOfFactorSources+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/Profile/MFA/SecurityStructureOfFactorSources+Wrap+Functions.swift index f628ec8ef..61da91bb3 100644 --- a/apple/Sources/Sargon/Extensions/Methods/Profile/MFA/SecurityStructureOfFactorSources+Wrap+Functions.swift +++ b/apple/Sources/Sargon/Extensions/Methods/Profile/MFA/SecurityStructureOfFactorSources+Wrap+Functions.swift @@ -1,19 +1,2 @@ import Foundation import SargonUniFFI - -extension SecurityStructureOfFactorSources { - public init( - metadata: SecurityStructureMetadata, - numberOfDaysUntilAutoConfirmation: UInt16, - matrixOfFactors: MatrixOfFactorSources - ) { - assert(matrixOfFactors.primaryRole.thresholdFactors.count >= matrixOfFactors.primaryRole.threshold) - assert(matrixOfFactors.recoveryRole.thresholdFactors.count >= matrixOfFactors.recoveryRole.threshold) - assert(matrixOfFactors.confirmationRole.thresholdFactors.count >= matrixOfFactors.confirmationRole.threshold) - self = newSecurityStructureOfFactorSourcesAutoInDays( - metadata: metadata, - numberOfDaysUntilAutoConfirmation: numberOfDaysUntilAutoConfirmation, - matrixOfFactors: matrixOfFactors - ) - } -} diff --git a/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift index b77bc7bff..c905be2df 100644 --- a/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift +++ b/apple/Sources/Sargon/Extensions/Methods/RET/TransactionManifest+Wrap+Functions.swift @@ -36,8 +36,6 @@ extension TransactionManifest { } public var summary: ManifestSummary { - get throws { - try transactionManifestSummary(manifest: self) - } + transactionManifestSummary(manifest: self) } } diff --git a/apple/Sources/Sargon/Extensions/Swiftified/Profile/MFA/SecurityShieldBuilder+Swifified.swift b/apple/Sources/Sargon/Extensions/Swiftified/Profile/MFA/SecurityShieldBuilder+Swifified.swift new file mode 100644 index 000000000..189231a54 --- /dev/null +++ b/apple/Sources/Sargon/Extensions/Swiftified/Profile/MFA/SecurityShieldBuilder+Swifified.swift @@ -0,0 +1,45 @@ +import SargonUniFFI + +extension SecurityShieldBuilder { + public typealias Factor = FactorSourceID + + /// Confirmation Role + public var numberOfDaysUntilAutoConfirm: UInt16 { + get { getNumberOfDaysUntilAutoConfirm() } + set { + precondition(newValue > 0, "Number of days until auto confirm must be greater than zero.") + setNumberOfDaysUntilAutoConfirm(numberOfDays: UInt16(newValue)) + } + } + + public var threshold: UInt8 { + get { getPrimaryThreshold() } + set { setThreshold(threshold: newValue) } + } + + public var primaryRoleThresholdFactors: [Factor] { + getPrimaryThresholdFactors() + } + + public var primaryRoleOverrideFactors: [Factor] { + getPrimaryOverrideFactors() + } + + public var recoveryRoleFactors: [Factor] { + getRecoveryFactors() + } + + public var confirmationRoleFactors: [Factor] { + getConfirmationFactors() + } + + /// Name of the shield + public var name: String { + get { + getName() + } + set { + setName(name: newValue) + } + } +} diff --git a/apple/Tests/TestCases/Prelude/Decimal192Tests.swift b/apple/Tests/TestCases/Prelude/Decimal192Tests.swift index c06117c50..0c5e1ee50 100644 --- a/apple/Tests/TestCases/Prelude/Decimal192Tests.swift +++ b/apple/Tests/TestCases/Prelude/Decimal192Tests.swift @@ -223,7 +223,7 @@ final class Decimal192Tests: Test { // Every number multiplied by zero, is zero... XCTAssertEqual(item * SUT.zero, SUT.zero) } - // ... incliding `max` and `min` + // ... including `max` and `min` XCTAssertEqual(SUT.max * 0, 0) XCTAssertEqual(SUT.min * 0, 0) @@ -239,7 +239,7 @@ final class Decimal192Tests: Test { // All numbers divided by themselves equals `one`... XCTAssertEqual(item / item, SUT.one) } - // ... incliding `max` and `min` + // ... including `max` and `min` XCTAssertEqual(SUT.max / SUT.max, SUT.one) XCTAssertEqual(SUT.min / SUT.min, SUT.one) } @@ -481,7 +481,7 @@ final class Decimal192Tests: Test { try largeDecimalsStrings.forEach(testLarge) XCTAssertLessThan(SUT.min.asDouble, SUT.max.asDouble) - XCTAssertNoThrow(try SUT("12345678987654321.000000000000000001").asDouble) + XCTAssertNoThrow(SUT("12345678987654321.000000000000000001").asDouble) } private var smallDecimalStrings: [String] { diff --git a/apple/Tests/TestCases/Profile/MFA/SecurityShieldsBuilderTests.swift b/apple/Tests/TestCases/Profile/MFA/SecurityShieldsBuilderTests.swift new file mode 100644 index 000000000..f5cc6f042 --- /dev/null +++ b/apple/Tests/TestCases/Profile/MFA/SecurityShieldsBuilderTests.swift @@ -0,0 +1,252 @@ +import CustomDump +import Foundation +import Sargon +import SargonUniFFI +import Testing + +// MARK: - ShieldTests +@Suite("ShieldBuilder") +struct ShieldTests { + @Test("name") + func name() { + let builder = SecurityShieldBuilder() + #expect(builder.name == "My Shield") + builder.name = "S.H.I.E.L.D" + #expect(builder.name == "S.H.I.E.L.D") + } + + @Test("threshold") + func threshold() { + let builder = SecurityShieldBuilder() + #expect(builder.threshold == 0) + builder.setThreshold(threshold: 42) + #expect(builder.threshold == 42) + } + + @Test("days") + func days() { + let builder = SecurityShieldBuilder() + #expect(builder.numberOfDaysUntilAutoConfirm == 14) + builder.setNumberOfDaysUntilAutoConfirm(numberOfDays: 237) + #expect(builder.numberOfDaysUntilAutoConfirm == 237) + } + + @Test("empty primary threshold") + func emptyThresholdFactors() { + let builder = SecurityShieldBuilder() + #expect(builder.primaryRoleThresholdFactors == []) + } + + @Test("empty primary override") + func emptyOverrideFactors() { + let builder = SecurityShieldBuilder() + #expect(builder.primaryRoleOverrideFactors == []) + } + + @Test("empty recovery") + func emptyRecoveryFactors() { + let builder = SecurityShieldBuilder() + #expect(builder.recoveryRoleFactors == []) + } + + @Test("empty confirmation") + func emptyConfirmationFactors() { + let builder = SecurityShieldBuilder() + #expect(builder.confirmationRoleFactors == []) + } + + @Test("primary override validation status trustedContact") + func primValidationStatusTrustedContact() { + let builder = SecurityShieldBuilder() + #expect(builder.validationForAdditionOfFactorSourceToPrimaryOverrideForEach(factorSources: [TrustedContactFactorSource.sample.asGeneral.id]).compactMap(\.reasonIfInvalid) == [FactorSourceValidationStatusReasonIfInvalid.nonBasic(SecurityShieldBuilderInvalidReason.PrimaryCannotContainTrustedContact)]) + } + + @Test("Auto lowering of threshold upon deletion") + func deleteFactorSourceFromPrimaryLowersThreshold() { + let builder = SecurityShieldBuilder() + let x: FactorSourceID = .sampleDevice + let y: FactorSourceID = .sampleLedger + let z: FactorSourceID = .sampleArculus + builder.addFactorSourceToPrimaryThreshold(factorSourceId: x) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: y) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: z) + builder.threshold = 3 + + builder.addFactorSourceToRecoveryOverride(factorSourceId: y) + #expect(builder.recoveryRoleFactors == [y]) + + #expect(builder.threshold == 3) + + builder.removeFactorFromPrimary(factorSourceId: x) + #expect(builder.threshold == 2) + + builder.removeFactorFromAllRoles(factorSourceId: y) + #expect(builder.recoveryRoleFactors == []) // assert `y` is removed from Recovery and Primary + #expect(builder.threshold == 1) + + builder.removeFactorFromPrimary(factorSourceId: z) + #expect(builder.threshold == 0) + #expect(builder.primaryRoleThresholdFactors == []) + } + + @Test("basic validation") + func basicValidation() throws { + let builder = SecurityShieldBuilder() + #expect(builder.validate() == .PrimaryRoleMustHaveAtLeastOneFactor) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleDevice) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleDevice) // did not get added, duplicates are not allowed + #expect(builder.primaryRoleThresholdFactors == [.sampleDevice]) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleDeviceOther) + + #expect(builder.validate() == .RecoveryRoleMustHaveAtLeastOneFactor) + builder.removeFactorFromPrimary(factorSourceId: .sampleDeviceOther) + builder.addFactorSourceToRecoveryOverride(factorSourceId: .sampleLedger) + + #expect(builder.validate() == .ConfirmationRoleMustHaveAtLeastOneFactor) + builder.addFactorSourceToConfirmationOverride(factorSourceId: .sampleArculus) + #expect(builder.validate() == nil) + #expect((try? builder.build()) != nil) + } + + @Test("primary role with threshold factors cannot have a threshold value of zero") + func primaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero() throws { + let builder = SecurityShieldBuilder() + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleLedger) + builder.threshold = 0 + #expect(builder.validate() == .PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero) + } + + @Test("cannot add forbidden FactorSourceKinds") + func preventAddOfForbiddenFactorSourceKinds() throws { + let builder = SecurityShieldBuilder() + + // Primary + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleTrustedContact) // Verboten + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleSecurityQuestions) // Verboten + + // Recovery + builder.addFactorSourceToRecoveryOverride(factorSourceId: .sampleSecurityQuestions) // Verboten + builder.addFactorSourceToRecoveryOverride(factorSourceId: .samplePassword) // Verboten + + // Confirmation + builder.addFactorSourceToConfirmationOverride(factorSourceId: .sampleTrustedContact) // Verboten + + #expect(builder.primaryRoleThresholdFactors.isEmpty) + #expect(builder.recoveryRoleFactors.isEmpty) + #expect(builder.confirmationRoleFactors.isEmpty) + } + + @Test("Primary can only contain one DeviceFactorSource") + func primaryCanOnlyContainOneDeviceFactorSourceThreshold() throws { + let builder = SecurityShieldBuilder() + let factor = FactorSourceId.sampleDevice + let other = FactorSourceId.sampleDeviceOther + builder.addFactorSourceToPrimaryThreshold(factorSourceId: factor) + builder.addFactorSourceToPrimaryOverride(factorSourceId: other) + #expect(builder.primaryRoleThresholdFactors == [factor]) + #expect(builder.primaryRoleOverrideFactors == []) + + builder.removeFactorFromPrimary(factorSourceId: factor) + + builder.addFactorSourceToPrimaryOverride(factorSourceId: factor) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: other) + #expect(builder.primaryRoleThresholdFactors == []) + #expect(builder.primaryRoleOverrideFactors == [factor]) + } + + @Test("Primary password never alone") + func primaryPasswordNeverAlone() { + let builder = SecurityShieldBuilder() + builder.addFactorSourceToPrimaryOverride(factorSourceId: .samplePassword) // not allowed + #expect(builder.primaryRoleOverrideFactors.isEmpty) + + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .samplePassword) + #expect(builder.validate() == .PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero) + builder.threshold = 0 + #expect(builder.validate() == .PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero) + builder.threshold = 1 + #expect(builder.validate() == .PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleLedger) + #expect(builder.validate() == .PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne) + builder.threshold = 2 + + builder.addFactorSourceToRecoveryOverride(factorSourceId: .sampleArculus) + builder.addFactorSourceToConfirmationOverride(factorSourceId: .sampleArculusOther) + + let shield = try! builder.build() + + #expect(shield.matrixOfFactors.primaryRole.overrideFactors.isEmpty) + #expect(shield.matrixOfFactors.primaryRole.threshold == 2) + #expect(shield.matrixOfFactors.primaryRole.thresholdFactors == [.samplePassword, .sampleLedger]) + } + + @Test("Build") + func build() throws { + let builder = SecurityShieldBuilder() + builder.setName(name: "S.H.I.E.L.D.") + builder.numberOfDaysUntilAutoConfirm = 42 + + #expect(builder.validate() == .PrimaryRoleMustHaveAtLeastOneFactor) + + // Primary + #expect(builder.threshold == 0) + builder.addFactorSourceToPrimaryThreshold(factorSourceId: .sampleDevice) // bumps threshold + #expect(builder.threshold == 1) + builder.addFactorSourceToPrimaryOverride(factorSourceId: .sampleArculus) + builder.addFactorSourceToPrimaryOverride(factorSourceId: .sampleArculusOther) + + // Recovery + builder.addFactorSourceToRecoveryOverride(factorSourceId: .sampleLedger) + builder.addFactorSourceToRecoveryOverride(factorSourceId: .sampleLedgerOther) + + // Confirmation + builder.addFactorSourceToConfirmationOverride(factorSourceId: .sampleDevice) + + builder.removeFactorFromPrimary(factorSourceId: .sampleArculusOther) + builder.removeFactorFromRecovery(factorSourceId: .sampleLedgerOther) + + // Validate + #expect(builder.validate() == nil) + + // Build + let shield0 = try builder.build() + let shield = try builder.build() + #expect(shield0 == shield) + + // Assert + #expect(shield.metadata.displayName == "S.H.I.E.L.D.") + #expect(shield.matrixOfFactors.primaryRole.overrideFactors == [.sampleArculus]) + #expect(shield.matrixOfFactors.primaryRole.thresholdFactors == [.sampleDevice]) + + #expect(shield.matrixOfFactors.recoveryRole.overrideFactors == [.sampleLedger]) + #expect(shield.matrixOfFactors.recoveryRole.thresholdFactors == []) + + #expect(shield.matrixOfFactors.confirmationRole.overrideFactors == [.sampleDevice]) + #expect(shield.matrixOfFactors.confirmationRole.thresholdFactors == []) + } +} + +#if DEBUG +extension FactorSourceID { + public static let sampleDevice = DeviceFactorSource.sample.asGeneral.id + public static let sampleDeviceOther = DeviceFactorSource.sampleOther.asGeneral.id + + public static let sampleLedger = LedgerHardwareWalletFactorSource.sample.asGeneral.id + public static let sampleLedgerOther = LedgerHardwareWalletFactorSource.sampleOther.asGeneral.id + + public static let sampleArculus = ArculusCardFactorSource.sample.asGeneral.id + public static let sampleArculusOther = ArculusCardFactorSource.sampleOther.asGeneral.id + + public static let samplePassword = PasswordFactorSource.sample.asGeneral.id + public static let samplePasswordOther = PasswordFactorSource.sampleOther.asGeneral.id + + public static let sampleOffDeviceMnemonic = OffDeviceMnemonicFactorSource.sample.asGeneral.id + public static let sampleOffDeviceMnemonicOther = OffDeviceMnemonicFactorSource.sampleOther.asGeneral.id + + public static let sampleTrustedContact = TrustedContactFactorSource.sample.asGeneral.id + public static let sampleTrustedContactOther = TrustedContactFactorSource.sampleOther.asGeneral.id + + public static let sampleSecurityQuestions = SecurityQuestionsNotProductionReadyFactorSource.sample.asGeneral.id + public static let sampleSecurityQuestionsOther = SecurityQuestionsNotProductionReadyFactorSource.sampleOther.asGeneral.id +} +#endif diff --git a/apple/Tests/TestCases/Profile/MFA/SecurityStructureOfFactorSourcesTests.swift b/apple/Tests/TestCases/Profile/MFA/SecurityStructureOfFactorSourcesTests.swift index 00ce01cdf..c89dee7ec 100644 --- a/apple/Tests/TestCases/Profile/MFA/SecurityStructureOfFactorSourcesTests.swift +++ b/apple/Tests/TestCases/Profile/MFA/SecurityStructureOfFactorSourcesTests.swift @@ -5,17 +5,6 @@ import SargonUniFFI import XCTest final class SecurityStructureOfFactorSourcesTests: Test { - func test_new_from_auto_in_days() { - let sut = SUT( - metadata: .sample, - numberOfDaysUntilAutoConfirmation: 10, - matrixOfFactors: .sample - ) - XCTAssertEqual(sut.numberOfEpochsUntilAutoConfirmation, 2880) - XCTAssertEqual(sut.metadata, .sample) - XCTAssertEqual(sut.matrixOfFactors, .sample) - } - func test_id() { eachSample { sut in XCTAssertEqual(sut.id, sut.metadata.id) diff --git a/apple/Tests/TestCases/RET/TransactionManifestTests.swift b/apple/Tests/TestCases/RET/TransactionManifestTests.swift index a074e5df3..12b0a642f 100644 --- a/apple/Tests/TestCases/RET/TransactionManifestTests.swift +++ b/apple/Tests/TestCases/RET/TransactionManifestTests.swift @@ -36,7 +36,7 @@ final class TransactionManifestTests: Test { } func test_manifest_summary() throws { - XCTAssertNoDifference(try SUT.sample.summary.addressesOfAccountsWithdrawnFrom, [AccountAddress.sampleMainnet]) + XCTAssertNoDifference(SUT.sample.summary.addressesOfAccountsWithdrawnFrom, [AccountAddress.sampleMainnet]) } func test_from_instructions_string_with_max_sbor_depth_is_ok() throws { diff --git a/crates/sargon-uniffi/Cargo.toml b/crates/sargon-uniffi/Cargo.toml index f5f3eb0e3..d164a90e1 100644 --- a/crates/sargon-uniffi/Cargo.toml +++ b/crates/sargon-uniffi/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sargon-uniffi" # Don't forget to update version in crates/sargon/Cargo.toml -version = "1.1.73" +version = "1.1.74" edition = "2021" build = "build.rs" @@ -80,8 +80,8 @@ itertools = { git = "https://github.com/rust-itertools/itertools/", rev = "98eca # enum-as-inner = "0.6.0" enum-as-inner = { git = "https://github.com/bluejekyll/enum-as-inner/", rev = "c15f6e5c4f98ec865e181ae1fff9fc13a1a2e4e2" } -# uniffi = "0.28.3" + unreleased changes -uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "2c003b16d1e70e1914b5d8ceb517eef3676cd187", features = [ +# uniffi = PRE "0.29.X" + Alex Cyons unmerged JNA Kotlin binding fix +uniffi = { git = "https://github.com/sajjon/uniffi-rs/", rev = "fe40d992b32103876112713cb5e8303b54647183", features = [ "cli", ] } @@ -115,6 +115,9 @@ pretty_assertions = { git = "https://github.com/rust-pretty-assertions/rust-pret base64 = { git = "https://github.com/marshallpierce/rust-base64.git", rev = "e14400697453bcc85997119b874bc03d9601d0af" } +# preinterpret = "0.2.0" +preinterpret = { git = "https://github.com/dhedey/preinterpret", rev = "6754b92bdead0ddd6f69fbee7d782180d6351605" } + # Fixes nasty iOS bug "_kSecMatchSubjectWholeString", see https://github.com/kornelski/rust-security-framework/issues/203 # This is a workaround to fix a bug with version 2.11.0 that added some symbols that are not available on iOS # The bug is fixed already but the fix is not released yet. https://github.com/kornelski/rust-security-framework/pull/204 @@ -124,8 +127,8 @@ security-framework-sys = "=2.10.0" [dev-dependencies] -# uniffi = "0.28.3" + unreleased changes -uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "2c003b16d1e70e1914b5d8ceb517eef3676cd187", features = [ +# uniffi = PRE "0.29.X" + Alex Cyons unmerged JNA Kotlin binding fix +uniffi = { git = "https://github.com/sajjon/uniffi-rs/", rev = "fe40d992b32103876112713cb5e8303b54647183", features = [ "bindgen-tests", ] } @@ -133,8 +136,8 @@ uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "2c003b16d1e70e1 actix-rt = { git = "https://github.com/actix/actix-net", rev = "57fd6ea8098d1f2d281c305fc331216c4fe1992e" } [build-dependencies] -# uniffi = "0.28.3" + unreleased changes -uniffi = { git = "https://github.com/mozilla/uniffi-rs/", rev = "2c003b16d1e70e1914b5d8ceb517eef3676cd187", features = [ +# uniffi = PRE "0.29.X" + Alex Cyons unmerged JNA Kotlin binding fix +uniffi = { git = "https://github.com/sajjon/uniffi-rs/", rev = "fe40d992b32103876112713cb5e8303b54647183", features = [ "build", ] } diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/decl_security_structure_of.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/decl_security_structure_of.rs deleted file mode 100644 index ac6a582eb..000000000 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/decl_security_structure_of.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::prelude::*; - -use sargon::HiddenConstructor as InternalHiddenConstructor; - -#[derive( - Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, uniffi::Record, -)] -pub struct HiddenConstructor; - -impl From for HiddenConstructor { - fn from(_value: InternalHiddenConstructor) -> Self { - HiddenConstructor - } -} -#[allow(clippy::from_over_into)] -impl Into for HiddenConstructor { - #[allow(clippy::from_over_into)] - fn into(self) -> InternalHiddenConstructor { - InternalHiddenConstructor - } -} - -macro_rules! decl_role_with_factors { - ( - $( - #[doc = $expr: expr] - )* - $role: ident, - $factor: ident - ) => { - paste! { - use sargon::[< $role RoleWith $factor s >] as [< Internal $role RoleWith $factor s >]; - - $( - #[doc = $expr] - )* - #[derive( - Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record, - )] - pub struct [< $role RoleWith $factor s >] { - /// Factors which are used in combination with other instances, amounting to at - /// least `threshold` many instances to perform some function with this role. - /// - /// # Implementation - /// Must allow duplicates, thus using `Vec` since at FactorSourceKind level - /// we might wanna use duplicates, allowing us to build a "template" - /// structure where a role might contain two `FactorSourceKind::TrustedContact`, - /// meaning an instance of this template at FactorSource level - /// (`SecurityStructureOfFactorSources`) will contain two different - /// `TrustedContactFactorSource`s. - pub threshold_factors: Vec<$factor>, - - /// How many threshold factors that must be used to perform some function with this role. - pub threshold: u8, - - /// Overriding / Super admin / "sudo" / God / factors, **ANY** - /// single of these factor which can perform the function of this role, - /// disregarding of `threshold`. - pub override_factors: Vec<$factor>, - } - } - }; -} - -pub(crate) use decl_role_with_factors; - -macro_rules! decl_matrix_of_factors { - ( - $( - #[doc = $expr: expr] - )* - $factor: ident - ) => { - paste! { - use sargon::[< MatrixOf $factor s >] as [< InternalMatrixOf $factor s >]; - - decl_role_with_factors!( - /// PrimaryRole is used for Signing Transactions. - Primary, - $factor - ); - - decl_role_with_factors!( - /// RecoveryRole is used to recover lost access to an entity. - Recovery, - $factor - ); - - decl_role_with_factors!( - /// ConfirmationRole is used to confirm recovery. - Confirmation, - $factor - ); - - $( - #[doc = $expr] - )* - #[derive( - Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record, - )] - pub struct [< MatrixOf $factor s >] { - /// Used for Signing transactions - pub primary_role: [< PrimaryRoleWith $factor s >], - - /// Used to initiate recovery - resetting the used Security Shield - /// of an entity. - pub recovery_role: [< RecoveryRoleWith $factor s >], - - /// To confirm recovery. - pub confirmation_role: [< ConfirmationRoleWith $factor s >], - } - } - }; -} - -pub(crate) use decl_matrix_of_factors; - -macro_rules! decl_security_structure_of { - ( - $( - #[doc = $expr: expr] - )* - $factor: ident, - ) => { - - decl_matrix_of_factors!($factor); - - paste! { - use sargon::[< SecurityStructureOf $factor s >] as [< InternalSecurityStructureOf $factor s >]; - - $( - #[doc = $expr] - )* - #[derive( - Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record, - )] - pub struct [< SecurityStructureOf $factor s >] { - /// Metadata of this Security Structure, such as globally unique and - /// stable identifier, creation date and user chosen label (name). - pub metadata: SecurityStructureMetadata, - - /// The amount of time until Confirmation Role is automatically - /// exercised, inputted by user in Days in UI, but translate it into - /// epochs ("block time"). - pub number_of_epochs_until_auto_confirmation: u64, - - /// The structure of factors to use for certain roles, Primary, Recovery - /// and Confirmation role. - pub matrix_of_factors: [< MatrixOf $factor s >], - } - } - }; -} - -pub(crate) use decl_security_structure_of; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/decl_matrix_macro.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/decl_matrix_macro.rs new file mode 100644 index 000000000..23fdb97b0 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/decl_matrix_macro.rs @@ -0,0 +1,88 @@ +use crate::prelude::*; + +use preinterpret::*; + +macro_rules! matrix_conversion { + ( + $(#[$attributes:meta])* + $factor_level: ident + ) => { + preinterpret::preinterpret! { + [!set! #internal_factor = [!ident! Internal $factor_level]] + [!set! #struct_name = [!ident! MatrixOf $factor_level s]] + [!set! #internal_struct_name = [!ident! Internal #struct_name]] + [!set! #primary_role_type = [!ident! PrimaryRoleWith $factor_level s ]] + [!set! #recovery_role_type = [!ident! RecoveryRoleWith $factor_level s ]] + [!set! #confirmation_role_type = [!ident! ConfirmationRoleWith $factor_level s ]] + + use sargon::#struct_name as #internal_struct_name; + use sargon::$factor_level as #internal_factor; + + $(#[$attributes])* + #[derive(Clone, PartialEq, Eq, Hash, uniffi::Record)] + pub struct #struct_name { + pub primary_role: #primary_role_type, + pub recovery_role: #recovery_role_type, + pub confirmation_role: #confirmation_role_type, + + pub number_of_days_until_auto_confirm: u16, + } + + delegate_debug_into!(#struct_name, #internal_struct_name); + + impl From<#internal_struct_name> for #struct_name { + fn from(value: #internal_struct_name) -> Self { + Self { + primary_role: value.primary().clone().into(), + recovery_role: value.recovery().clone().into(), + confirmation_role: value.confirmation().clone().into(), + number_of_days_until_auto_confirm: value + .number_of_days_until_auto_confirm, + } + } + } + + impl #struct_name { + pub fn into_internal(&self) -> #internal_struct_name { + unsafe { + #internal_struct_name::unbuilt_with_roles_and_days( + self.primary_role.clone().into(), + self.recovery_role.clone().into(), + self.confirmation_role.clone().into(), + self.number_of_days_until_auto_confirm, + ) + } + } + } + impl From<#struct_name> for #internal_struct_name { + fn from(value: #struct_name) -> Self { + value.into_internal() + } + } + + impl HasSampleValues for #struct_name { + fn sample() -> Self { + #internal_struct_name::sample().into() + } + fn sample_other() -> Self { + #internal_struct_name::sample_other().into() + } + } + + [!set! #fn_name_prefix = new_[!snake! #struct_name]] + + #[uniffi::export] + pub fn [!ident! #fn_name_prefix _sample ]() -> #struct_name { + #struct_name::sample() + } + + #[uniffi::export] + pub fn [!ident! #fn_name_prefix _sample_other ]() -> #struct_name { + #struct_name::sample_other() + } + + } + }; +} + +pub(crate) use matrix_conversion; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs new file mode 100644 index 000000000..454582a1d --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs @@ -0,0 +1,8 @@ +use crate::prelude::*; + +use super::decl_matrix_macro::matrix_conversion; + +matrix_conversion!( + /// Matrix of `FactorInstance`s containing the primary, recovery, and confirmation roles with `FactorInstance`s + FactorInstance +); diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs new file mode 100644 index 000000000..7f6c3f651 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; + +use super::decl_matrix_macro::matrix_conversion; + +matrix_conversion!( + /// Matrix of `FactorSourceID`s containing the primary, recovery, and confirmation roles with `FactorSourceID`s + FactorSourceID +); + +macro_rules! export_sample_config { + ($config_ident:literal) => { + paste! { + #[uniffi::export] + pub fn []() -> MatrixOfFactorSourceIDs { + ::[< sample_config_ $config_ident>]().into() + } + } + }; +} + +pub(crate) use export_sample_config; + +export_sample_config!(1_1); +export_sample_config!(1_2); +export_sample_config!(1_3); +export_sample_config!(1_4); +export_sample_config!(1_5); +export_sample_config!(2_1); +export_sample_config!(2_2); +export_sample_config!(2_3); +export_sample_config!(2_4); +export_sample_config!(3_0); +export_sample_config!(4_0); +export_sample_config!(5_1); +export_sample_config!(5_2); +export_sample_config!(6_0); +export_sample_config!(7_0); +export_sample_config!(8_0); +export_sample_config!(9_0); diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs new file mode 100644 index 000000000..94f3f52b0 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; + +use super::decl_matrix_macro::matrix_conversion; + +matrix_conversion!( + /// Matrix of `FactorSource`s containing the primary, recovery, and confirmation roles with `FactorSource`s + FactorSource +); + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/mod.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/mod.rs new file mode 100644 index 000000000..d38c5eb12 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/matrices/mod.rs @@ -0,0 +1,10 @@ +#[macro_use] +mod decl_matrix_macro; + +mod matrix_of_factor_instances; +mod matrix_of_factor_source_ids; +mod matrix_of_factor_sources; + +pub use matrix_of_factor_instances::*; +pub use matrix_of_factor_source_ids::*; +pub use matrix_of_factor_sources::*; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/mod.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/mod.rs index 4a9f0c16d..8c0137ec5 100644 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/mod.rs +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/mod.rs @@ -1,13 +1,18 @@ -mod decl_security_structure_of; +mod matrices; +mod models; +mod roles; +mod security_shield_builder; +mod security_shield_builder_invalid_reason; +mod security_shield_prerequisites_status; mod security_structure_id; mod security_structure_metadata; -mod security_structure_of_factor_instances; -mod security_structure_of_factor_source_ids; -mod security_structure_of_factor_sources; +mod security_structures; -pub use decl_security_structure_of::*; +pub use matrices::*; +pub use models::*; +pub use roles::*; +pub use security_shield_builder_invalid_reason::*; +pub use security_shield_prerequisites_status::*; pub use security_structure_id::*; pub use security_structure_metadata::*; -pub use security_structure_of_factor_instances::*; -pub use security_structure_of_factor_source_ids::*; -pub use security_structure_of_factor_sources::*; +pub use security_structures::*; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/models/factor_source_in_role_builder_validation_status.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/factor_source_in_role_builder_validation_status.rs new file mode 100644 index 000000000..f00dabf0f --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/factor_source_in_role_builder_validation_status.rs @@ -0,0 +1,58 @@ +use sargon::AsShieldBuilderViolation; + +use crate::prelude::*; + +/// The "validation result" of a `FactorSourceID` in a `Role`, if +/// it we were to add it to a role list. +#[derive(Clone, Debug, PartialEq, uniffi::Record)] +pub struct FactorSourceValidationStatus { + pub role: RoleKind, + pub factor_source_id: FactorSourceID, + pub reason_if_invalid: Option, +} + +/// The reason why a `FactorSourceID` is invalid if it were +/// to be added into a factor list for some role. +#[derive(Clone, Debug, PartialEq, uniffi::Enum)] +pub enum FactorSourceValidationStatusReasonIfInvalid { + BasicViolation(String), + NonBasic(SecurityShieldBuilderInvalidReason), +} + +impl From + for FactorSourceValidationStatus +{ + fn from(val: sargon::FactorSourceInRoleBuilderValidationStatus) -> Self { + let reason_if_invalid: Option< + FactorSourceValidationStatusReasonIfInvalid, + > = { + match val.validation { + Ok(_) => None, + Err(sargon::RoleBuilderValidation::BasicViolation(b)) => Some( + FactorSourceValidationStatusReasonIfInvalid::BasicViolation( + format!("{:?}", b), + ), + ), + Err(sargon::RoleBuilderValidation::ForeverInvalid(v)) => v + .as_shield_validation() + .map(SecurityShieldBuilderInvalidReason::from) + .map(|x| { + FactorSourceValidationStatusReasonIfInvalid::NonBasic(x) + }), + Err(sargon::RoleBuilderValidation::NotYetValid(v)) => ( + val.role, v, + ) + .as_shield_validation() + .map(SecurityShieldBuilderInvalidReason::from) + .map(|x| { + FactorSourceValidationStatusReasonIfInvalid::NonBasic(x) + }), + } + }; + FactorSourceValidationStatus { + role: val.role.into(), + factor_source_id: val.factor_source_id.into(), + reason_if_invalid, + } + } +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/models/mod.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/mod.rs new file mode 100644 index 000000000..0883645ea --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/mod.rs @@ -0,0 +1,5 @@ +mod factor_source_in_role_builder_validation_status; +mod role_kind; + +pub use factor_source_in_role_builder_validation_status::*; +pub use role_kind::*; diff --git a/crates/sargon-uniffi/src/signing/role_kind.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/role_kind.rs similarity index 66% rename from crates/sargon-uniffi/src/signing/role_kind.rs rename to crates/sargon-uniffi/src/profile/mfa/security_structures/models/role_kind.rs index 54cb45482..d2d8f2bc0 100644 --- a/crates/sargon-uniffi/src/signing/role_kind.rs +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/models/role_kind.rs @@ -1,7 +1,10 @@ use crate::prelude::*; use sargon::RoleKind as InternalRoleKind; -#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Enum)] +/// A discriminator of a role in a matrix of Factors. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, Hash, InternalConversion, uniffi::Enum, +)] pub enum RoleKind { /// The primary role of some matrix of factors Primary, diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/decl_role_macro.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/decl_role_macro.rs new file mode 100644 index 000000000..f27547b08 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/decl_role_macro.rs @@ -0,0 +1,194 @@ +use crate::prelude::*; + +// This macro generates "Role" types, for each RoleKind: Primary, Recovery, Confirmation +// for the specified "Factor Level", so input is `FactorSource` or `FactorInstance` +// or `FactorSourceID` etc. +// It will generate the following: +// struct PrimaryRoleWithFactor<$FACTOR_LEVEL> { +// threshold: u8, +// threshold_factors: Vec<$FACTOR_LEVEL>, +// override_factors: Vec<$FACTOR_LEVEL>, +// } +// and `struct RecoveryRoleWithFactor<$FACTOR_LEVEL>` +// and `struct ConfirmationRoleWithFactor<$FACTOR_LEVEL>` +// +// And it will generate: +// * `From> for PrimaryRoleWithFactor<$FACTOR_LEVEL>` +// * `From> for RecoveryRoleWithFactor<$FACTOR_LEVEL>` +// * `From> for ConfirmationRoleWithFactor<$FACTOR_LEVEL>` +// +// And analogously it will impl the inverse conversion: +// * `From> for InternalPrimaryRoleWithFactor<$FACTOR_LEVEL>` +// * `From> for InternalRecoveryRoleWithFactor<$FACTOR_LEVEL>` +// * `From> for InternalConfirmationRoleWithFactor<$FACTOR_LEVEL>` +// +// Furthermore it will generate `HasSampleValues` impl for each of the generated structs. +// and also uniffi export them. +macro_rules! role_conversion { + ( + $( + #[doc = $expr: expr] + )* + $factor_level:ty + ) => { + paste! { + use sargon::$factor_level as [< Internal $factor_level>]; + } + role_conversion_inner!( + for: + $( + #[doc = $expr] + )* + Primary $factor_level + ); + role_conversion_inner!( + for: + $( + #[doc = $expr] + )* + Recovery $factor_level) + ; + role_conversion_inner!( + for: + $( + #[doc = $expr] + )* + Confirmation $factor_level + ); + }; +} + +pub(crate) use role_conversion; + +macro_rules! role_conversion_inner { + // Impl From -> crate + (from_internal: $internal:ident, $uniffi:ident) => { + impl From<$internal> for $uniffi { + fn from(value: $internal) -> Self { + Self { + threshold: value.get_threshold(), + threshold_factors: value + .get_threshold_factors() + .into_iter() + .map(|x| x.clone().into()) + .collect(), + override_factors: value + .get_override_factors() + .into_iter() + .map(|x| x.clone().into()) + .collect(), + } + } + } + }; + + // Impl From -> Internal + (to_internal: $internal_factor:ty => $uniffi:ident, $internal:ident) => { + impl $uniffi { + pub fn into_internal(&self) -> $internal { + unsafe { + <$internal>::unbuilt_with_factors( + self.threshold, + self.threshold_factors.clone().into_iter().map(|x| Into::<$internal_factor>::into(x.clone())).collect::>(), + self.override_factors.clone().into_iter().map(|x| Into::<$internal_factor>::into(x.clone())).collect::>(), + ) + } + } + } + impl From<$uniffi> for $internal { + fn from(value: $uniffi) -> Self { + value.into_internal() + } + } + }; + + // Impl `From for X` and `From for InternalX` + // and impl `HasSampleValues` for X` and UniFFI export `new_X_sample` and `new_X_sample_other`. + (impl_from: $factor_level:ty => $uniffi_name:ident => $internal_name:ident ) => { + role_conversion_inner!( + from_internal: $internal_name, $uniffi_name + ); + role_conversion_inner!( + to_internal: $factor_level => $uniffi_name, $internal_name + ); + + impl HasSampleValues for $uniffi_name { + fn sample() -> Self { + $internal_name::sample().into() + } + fn sample_other() -> Self { + $internal_name::sample_other().into() + } + } + + paste! { + #[uniffi::export] + pub fn [< new_ $uniffi_name:snake _ sample >]() -> $uniffi_name { + $uniffi_name::sample() + } + + #[uniffi::export] + pub fn [< new_ $uniffi_name:snake _ sample_other >]() -> $uniffi_name { + $uniffi_name::sample_other() + } + } + }; + + // Declare the struct and impl `From + // and by recursively calling `role_conversion_inner` also impl + // `From` conversions. + ( + struct: + $( + #[doc = $expr: expr] + )* + $struct_name:ident, $factor_level:ty + ) => { + $( + #[doc = $expr] + )* + #[derive(Clone, PartialEq, Eq, Hash, uniffi::Record)] + pub struct $struct_name { + + /// How many threshold factors that must be used to perform some function with + /// this role. + pub threshold: u8, + + /// Factors which are used in combination with other factors, amounting to at + /// least `threshold` many factors to perform some function with this role. + pub threshold_factors: Vec<$factor_level>, + + /// Overriding / Super admin / "sudo" / God / factors, **ANY** + /// single of these factor which can perform the function of this role, + /// disregarding of `threshold`. + pub override_factors: Vec<$factor_level>, + } + paste! { + use sargon::$struct_name as [< Internal $struct_name>]; + delegate_debug_into!($struct_name, [< Internal $struct_name>]); + role_conversion_inner!( + impl_from: [< Internal $factor_level>] => $struct_name => [< Internal $struct_name >] + ); + } + }; + ( + for: + $( + #[doc = $expr: expr] + )* + $role:ident $factor_level:ty + ) => { + paste! { + role_conversion_inner!( + struct: + $( + #[doc = $expr] + )* + [< $role RoleWith $factor_level s >], + $factor_level + ); + } + }; +} + +pub(crate) use role_conversion_inner; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/mod.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/mod.rs new file mode 100644 index 000000000..212ff849c --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/mod.rs @@ -0,0 +1,10 @@ +#[macro_use] +mod decl_role_macro; + +mod roles_factor_instances; +mod roles_factor_source_ids; +mod roles_factor_sources; + +pub use roles_factor_instances::*; +pub use roles_factor_source_ids::*; +pub use roles_factor_sources::*; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_instances.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_instances.rs new file mode 100644 index 000000000..7ffd948ce --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_instances.rs @@ -0,0 +1,8 @@ +use crate::prelude::*; + +use super::decl_role_macro::role_conversion; + +role_conversion!( + /// Role of FactorInstances + FactorInstance +); diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_source_ids.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_source_ids.rs new file mode 100644 index 000000000..6b2c7a469 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_source_ids.rs @@ -0,0 +1,8 @@ +use crate::prelude::*; + +use super::decl_role_macro::role_conversion; + +role_conversion!( + /// Role of FactorSourceIDs + FactorSourceID +); diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_sources.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_sources.rs new file mode 100644 index 000000000..0cf74bce3 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/roles/roles_factor_sources.rs @@ -0,0 +1,8 @@ +use crate::prelude::*; + +use super::decl_role_macro::role_conversion; + +role_conversion!( + /// Role of `FactorSource`s + FactorSource +); diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder.rs new file mode 100644 index 000000000..4d334469c --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder.rs @@ -0,0 +1,679 @@ +#![allow(clippy::new_without_default)] +#![allow(dead_code)] +#![allow(unused_variables)] + +use std::{ + borrow::Borrow, + sync::{Arc, RwLock}, +}; + +use sargon::{IndexSet, MatrixBuilder}; + +use crate::prelude::*; + +/// A builder of `SecurityStructureOfFactorSourceIds` a.k.a. `SecurityShield`, +/// which contains a MatrixOfFactorSourceIds - with primary, recovery, and +/// confirmation roles. +#[derive(Debug, uniffi::Object)] +pub struct SecurityShieldBuilder { + wrapped: RwLock, +} + +#[uniffi::export] +impl SecurityShieldBuilder { + #[uniffi::constructor] + pub fn new() -> Arc { + Arc::new(Self { + wrapped: RwLock::new(sargon::SecurityShieldBuilder::new()), + }) + } +} + +impl SecurityShieldBuilder { + fn get( + &self, + access: impl Fn(&sargon::SecurityShieldBuilder) -> R, + ) -> R { + let binding = self.wrapped.read().unwrap(); + access(&binding) + } + + fn set( + &self, + mut write: impl FnMut( + &mut sargon::SecurityShieldBuilder, + ) -> &sargon::SecurityShieldBuilder, + ) { + let mut binding = self.wrapped.write().expect("No poison"); + _ = write(&mut binding); + } + + fn validation_for_addition_of_factor_source_by_calling( + &self, + factor_sources: Vec, + call: impl Fn( + &sargon::SecurityShieldBuilder, + Vec, + ) + -> Vec, + ) -> Vec { + let input = factor_sources + .clone() + .into_iter() + .map(Into::::into) + .collect_vec(); + + self.get(|builder| { + call(builder, input.clone()) + .into_iter() + .map(Into::::into) + .collect() + }) + } +} + +impl SecurityShieldBuilder { + fn get_factors( + &self, + access: impl Fn( + &sargon::SecurityShieldBuilder, + ) -> Vec, + ) -> Vec { + self.get(|builder| { + let factors = access(builder); + factors + .into_iter() + .map(crate::FactorSourceID::from) + .collect::>() + }) + } +} +// ==================== +// ==== GET / READ ==== +// ==================== +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn get_primary_threshold(&self) -> u8 { + self.get(|builder| builder.get_threshold()) + } + + pub fn get_number_of_days_until_auto_confirm(&self) -> u16 { + self.get(|builder| builder.get_number_of_days_until_auto_confirm()) + } + + pub fn get_name(&self) -> String { + self.get(|builder| builder.get_name()) + } + + pub fn get_primary_threshold_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_primary_threshold_factors()) + } + + pub fn get_primary_override_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_primary_override_factors()) + } + + pub fn get_recovery_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_recovery_factors()) + } + + pub fn get_confirmation_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_confirmation_factors()) + } +} + +// ==================== +// ===== MUTATION ===== +// ==================== +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn set_name(&self, name: String) { + self.set(|builder| builder.set_name(&name)); + } + + pub fn remove_factor_from_all_roles( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder + .remove_factor_from_all_roles(factor_source_id.clone().into()) + }) + } + + pub fn remove_factor_from_primary(&self, factor_source_id: FactorSourceID) { + self.set(|builder| { + builder.remove_factor_from_primary(factor_source_id.clone().into()) + }) + } + + pub fn remove_factor_from_recovery( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.remove_factor_from_recovery(factor_source_id.clone().into()) + }) + } + + pub fn remove_factor_from_confirmation( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.remove_factor_from_confirmation( + factor_source_id.clone().into(), + ) + }) + } + + pub fn set_threshold(&self, threshold: u8) { + self.set(|builder| builder.set_threshold(threshold)) + } + + pub fn set_number_of_days_until_auto_confirm(&self, number_of_days: u16) { + self.set(|builder| { + builder.set_number_of_days_until_auto_confirm(number_of_days) + }) + } + + /// Adds the factor source to the primary role threshold list. + /// + /// Also sets the threshold to 1 if this is the first factor set and if + /// the threshold was 0. + pub fn add_factor_source_to_primary_threshold( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.add_factor_source_to_primary_threshold( + factor_source_id.clone().into(), + ) + }) + } + + pub fn add_factor_source_to_primary_override( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.add_factor_source_to_primary_override( + factor_source_id.clone().into(), + ) + }) + } + + pub fn add_factor_source_to_recovery_override( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.add_factor_source_to_recovery_override( + factor_source_id.clone().into(), + ) + }) + } + + pub fn add_factor_source_to_confirmation_override( + &self, + factor_source_id: FactorSourceID, + ) { + self.set(|builder| { + builder.add_factor_source_to_confirmation_override( + factor_source_id.clone().into(), + ) + }) + } + + pub fn reset_recovery_and_confirmation_role_state(&self) { + self.set(|builder| builder.reset_recovery_and_confirmation_role_state()) + } +} + +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_primary_threshold_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_primary_threshold_is_valid_or_can_be( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_primary_override_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_primary_override_is_fully_valid( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_primary_override_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_primary_override_is_valid_or_can_be( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_recovery_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_recovery_is_valid_or_can_be( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| { + builder + .addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + factor_source_kind.clone().into(), + ) + }) + } + + pub fn addition_of_factor_source_of_kind_to_confirmation_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_confirmation_is_valid_or_can_be( + factor_source_kind.clone().into(), + ) + ) + } + + pub fn addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get(|builder| + builder.addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + factor_source_kind.clone().into(), + ) + ) + } +} + +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder + .validation_for_addition_of_factor_source_to_primary_threshold_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_primary_override_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_recovery_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder + .validation_for_addition_of_factor_source_to_recovery_override_for_each(input) + }, + ) + } + pub fn validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_confirmation_override_for_each( + input, + ) + }, + ) + } +} + +#[uniffi::export] +impl SecurityShieldBuilder { + pub fn validate(&self) -> Option { + self.get(|builder| builder.validate().map(|x| x.into())) + } + + pub fn build( + &self, + ) -> Result< + SecurityStructureOfFactorSourceIDs, + SecurityShieldBuilderInvalidReason, + > { + self.get(|builder| builder.build()) + .map(|shield| shield.into()) + .map_err(|x| x.into()) + } +} + +impl FactorSourceID { + pub fn new(inner: impl Borrow) -> Self { + Self::from(*inner.borrow()) + } +} + +#[cfg(test)] +impl FactorSourceID { + pub fn sample_device() -> Self { + Self::new(sargon::FactorSourceID::sample_device()) + } + + pub fn sample_device_other() -> Self { + Self::new(sargon::FactorSourceID::sample_device_other()) + } + + pub fn sample_ledger() -> Self { + Self::new(sargon::FactorSourceID::sample_ledger()) + } + + pub fn sample_ledger_other() -> Self { + Self::new(sargon::FactorSourceID::sample_ledger_other()) + } + + pub fn sample_arculus() -> Self { + Self::new(sargon::FactorSourceID::sample_arculus()) + } + + pub fn sample_arculus_other() -> Self { + Self::new(sargon::FactorSourceID::sample_arculus_other()) + } + + pub fn sample_password() -> Self { + Self::new(sargon::FactorSourceID::sample_password()) + } + + pub fn sample_password_other() -> Self { + Self::new(sargon::FactorSourceID::sample_password_other()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityShieldBuilder; + + #[test] + fn test() { + let sut = SUT::new(); + + assert_eq!(sut.get_name(), "My Shield"); + sut.set_name("S.H.I.E.L.D.".to_owned()); + + assert_eq!(sut.get_number_of_days_until_auto_confirm(), 14); + sut.set_number_of_days_until_auto_confirm(u16::MAX); + assert_eq!(sut.get_number_of_days_until_auto_confirm(), u16::MAX); + // Primary + let sim_prim = + sut.validation_for_addition_of_factor_source_to_primary_override_for_each(vec![ + FactorSourceID::sample_arculus(), + ]); + + let sim_prim_threshold = sut + .validation_for_addition_of_factor_source_to_primary_threshold_for_each(vec![ + FactorSourceID::sample_arculus(), + ]); + + let sim_kind_prim = sut + .addition_of_factor_source_of_kind_to_primary_override_is_fully_valid( + FactorSourceKind::Device, + ); + + let sim_kind_prim_threshold = sut + .addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid( + FactorSourceKind::Device, + ); + + sut.add_factor_source_to_primary_threshold( + // should also bump threshold to 1 + FactorSourceID::sample_device(), + ); + assert_eq!(sut.get_primary_threshold(), 1); + + sut.add_factor_source_to_primary_threshold( + // should NOT bump threshold + FactorSourceID::sample_password_other(), + ); + assert_eq!(sut.get_primary_threshold(), 1); + sut.remove_factor_from_primary(FactorSourceID::sample_password_other()); + + assert_eq!( + sut.get_primary_threshold_factors(), + vec![FactorSourceID::sample_device()] + ); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_arculus(), + ); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_arculus_other(), + ); + + assert_eq!( + sut.get_primary_override_factors(), + vec![ + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other() + ] + ); + + // Recovery + let sim_rec = + sut.validation_for_addition_of_factor_source_to_recovery_override_for_each(vec![ + FactorSourceID::sample_ledger(), + ]); + + let sim_kind_rec = sut + .clone() + .addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + FactorSourceKind::ArculusCard, + ); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ); + + assert_eq!( + sut.get_recovery_factors(), + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other() + ] + ); + sut.reset_recovery_and_confirmation_role_state(); + assert_eq!(sut.get_recovery_factors(), vec![]); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ); + + assert_eq!( + sut.get_recovery_factors(), + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other() + ] + ); + + // Confirmation + let sim_conf = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each(vec![ + FactorSourceID::sample_device(), + ]); + + let sim_kind_conf = sut + .clone() + .addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + FactorSourceKind::ArculusCard, + ); + + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ); + + assert_eq!( + sut.get_confirmation_factors(), + vec![FactorSourceID::sample_device(),] + ); + + assert_ne!( + sim_prim, + sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + vec![ + FactorSourceID::sample_arculus(), + ]) + ); + + assert_ne!( + sim_prim_threshold, + sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + vec![ + FactorSourceID::sample_arculus() + ]) + ); + + assert_ne!( + sim_rec, + sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + vec![ + FactorSourceID::sample_ledger(), + ]) + ); + + assert_ne!( + sim_conf, + sut.validation_for_addition_of_factor_source_to_confirmation_override_for_each( + vec![ + FactorSourceID::sample_device(), + ]) + ); + + assert_ne!( + sim_kind_prim, + sut.addition_of_factor_source_of_kind_to_primary_override_is_fully_valid( + FactorSourceKind::Device, + ) + ); + + assert_ne!( + sim_kind_prim_threshold, + sut.addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid( + FactorSourceKind::Device, + ) + ); + + assert_eq!( + sim_kind_rec, + sut.addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + FactorSourceKind::ArculusCard, + ) + ); + + assert_eq!( + sim_kind_conf, + sut.addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + FactorSourceKind::ArculusCard, + ) + ); + + sut.remove_factor_from_all_roles( + FactorSourceID::sample_arculus_other(), + ); + sut.remove_factor_from_all_roles(FactorSourceID::sample_ledger_other()); + + let f = FactorSourceID::sample_ledger_other(); + let xs = sut.get_primary_override_factors(); + sut.add_factor_source_to_primary_override(f.clone()); + sut.remove_factor_from_primary(f.clone()); + assert_eq!(xs, sut.get_primary_override_factors()); + + let xs = sut.get_recovery_factors(); + sut.clone() + .add_factor_source_to_recovery_override(f.clone()); + sut.remove_factor_from_recovery(f.clone()); + assert_eq!(xs, sut.get_recovery_factors()); + + let xs = sut.get_confirmation_factors(); + sut.clone() + .add_factor_source_to_confirmation_override(f.clone()); + sut.remove_factor_from_confirmation(f.clone()); + assert_eq!(xs, sut.get_confirmation_factors()); + + let v0 = sut.validate(); + let v1 = sut.validate(); // can call validate many times! + assert_eq!(v0, v1); + + let shield0 = sut.build().unwrap(); + let shield = sut.build().unwrap(); // can call build many times! + assert_eq!(shield0, shield); + + assert_eq!(shield.metadata.display_name.value, "S.H.I.E.L.D."); + assert_eq!( + shield.matrix_of_factors.primary_role.override_factors, + vec![FactorSourceID::sample_arculus()] + ); + assert_eq!( + shield.matrix_of_factors.recovery_role.override_factors, + vec![FactorSourceID::sample_ledger()] + ); + assert_eq!( + shield.matrix_of_factors.confirmation_role.override_factors, + vec![FactorSourceID::sample_device()] + ); + } +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs new file mode 100644 index 000000000..16a0a488b --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs @@ -0,0 +1,77 @@ +use crate::prelude::*; +use sargon::SecurityShieldBuilderInvalidReason as InternalSecurityShieldBuilderInvalidReason; + +use thiserror::Error as ThisError; + +#[repr(u32)] +#[derive( + Clone, Debug, ThisError, PartialEq, InternalConversion, uniffi::Error, +)] +pub enum SecurityShieldBuilderInvalidReason { + #[error("Shield name is invalid")] + ShieldNameInvalid, + + #[error("The number of days until auto confirm must be greater than zero")] + NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, + + #[error("Recovery and confirmation factors overlap. No factor may be used in both the recovery and confirmation roles")] + RecoveryAndConfirmationFactorsOverlap, + + #[error("The single factor used in the primary role must not be used in any other role")] + SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole, + + // ========================= + // NotYetValidReason + // ========================= + #[error("PrimaryRole must have at least one factor")] + PrimaryRoleMustHaveAtLeastOneFactor, + + #[error("RecoveryRole must have at least one factor")] + RecoveryRoleMustHaveAtLeastOneFactor, + + #[error("ConfirmationRole must have at least one factor")] + ConfirmationRoleMustHaveAtLeastOneFactor, + + #[error( + "Primary role with password in threshold list must have another factor" + )] + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + + #[error( + "Primary role with threshold factors cannot have a threshold of zero" + )] + PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero, + + #[error("Primary role with password in threshold list must have threshold greater than one")] + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + + #[error("Threshold higher than threshold factors len")] + ThresholdHigherThanThresholdFactorsLen, + + // ================================ + // ForeverInvalidReason + // ================================ + #[error("Factor source already present")] + FactorSourceAlreadyPresent, + + #[error("Primary role cannot have multiple devices")] + PrimaryCannotHaveMultipleDevices, + + #[error("Primary role cannot have password in override list")] + PrimaryCannotHavePasswordInOverrideList, + + #[error("Primary role cannot contain Security Questions")] + PrimaryCannotContainSecurityQuestions, + + #[error("Primary role cannot contain Trusted Contact")] + PrimaryCannotContainTrustedContact, + + #[error("Recovery role Security Questions not supported")] + RecoveryRoleSecurityQuestionsNotSupported, + + #[error("Recovery role password not supported")] + RecoveryRolePasswordNotSupported, + + #[error("Confirmation role cannot contain Trusted Contact")] + ConfirmationRoleTrustedContactNotSupported, +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs new file mode 100644 index 000000000..f3ef1c3b8 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs @@ -0,0 +1,19 @@ +use crate::prelude::*; +use sargon::SecurityShieldPrerequisitesStatus as InternalSecurityShieldPrerequisitesStatus; + +/// An enum representing the status of the prerequisites for building a Security Shield. +/// This is, whether the user has the necessary factor sources to build a Security Shield. +#[derive( + Clone, Copy, Debug, PartialEq, Eq, InternalConversion, uniffi::Enum, +)] +pub enum SecurityShieldPrerequisitesStatus { + /// A Security Shield can be built with the current Factor Sources available. + Sufficient, + + /// At least one hardware Factor Source must be added in order to build a Shield. + /// Note: this doesn't mean that after adding a hardware Factor Source we would have `Sufficient` status. + HardwareRequired, + + /// One more Factor Source, of any category, must be added in order to build a Shield. + AnyRequired, +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_metadata.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_metadata.rs index 38f2ede0d..58e5517b6 100644 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_metadata.rs +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_metadata.rs @@ -9,6 +9,11 @@ pub struct SecurityStructureMetadata { pub last_updated_on: Timestamp, } +delegate_debug_into!( + SecurityStructureMetadata, + InternalSecurityStructureMetadata +); + #[uniffi::export] pub fn new_security_structure_metadata_sample() -> SecurityStructureMetadata { InternalSecurityStructureMetadata::sample().into() diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs deleted file mode 100644 index 045453a7e..000000000 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_source_ids.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::prelude::*; - -decl_security_structure_of!( - /// A security structure at FactorSourceID level, this is - /// what is serialized and store into Profile, we convert - /// into this structure from `SecurityStructureOfFactorSources`. - FactorSourceID, -); - -decl_vec_samples_for!( - SecurityStructuresOfFactorSourceIDs, - SecurityStructureOfFactorSourceIDs -); - -#[uniffi::export] -pub fn new_security_structure_of_factor_source_ids_sample( -) -> SecurityStructureOfFactorSourceIDs { - InternalSecurityStructureOfFactorSourceIDs::sample().into() -} - -#[uniffi::export] -pub fn new_security_structure_of_factor_source_ids_sample_other( -) -> SecurityStructureOfFactorSourceIDs { - InternalSecurityStructureOfFactorSourceIDs::sample_other().into() -} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs deleted file mode 100644 index fce80ecd6..000000000 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_sources.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; - -decl_security_structure_of!( - /// Security structure at `FactorSource` level. - /// This is what user view, creates and manages. - /// - /// Before it gets saved into Profile gets converted into - /// `SecurityStructureOfFactorSourceIDs` - FactorSource, -); - -#[uniffi::export] -pub fn new_security_structure_of_factor_sources_sample( -) -> SecurityStructureOfFactorSources { - InternalSecurityStructureOfFactorSources::sample().into() -} - -#[uniffi::export] -pub fn new_security_structure_of_factor_sources_sample_other( -) -> SecurityStructureOfFactorSources { - InternalSecurityStructureOfFactorSources::sample_other().into() -} - -#[uniffi::export] -pub fn new_security_structure_of_factor_sources_auto_in_days( - metadata: SecurityStructureMetadata, - number_of_days_until_auto_confirmation: u16, - matrix_of_factors: MatrixOfFactorSources, -) -> SecurityStructureOfFactorSources { - InternalSecurityStructureOfFactorSources::new_with_days( - metadata.into_internal(), - number_of_days_until_auto_confirmation, - matrix_of_factors.into_internal(), - ) - .into() -} - -#[uniffi::export] -pub fn new_matrix_of_factor_sources_sample() -> MatrixOfFactorSources { - InternalMatrixOfFactorSources::sample().into() -} - -#[uniffi::export] -pub fn new_matrix_of_factor_sources_sample_other() -> MatrixOfFactorSources { - InternalMatrixOfFactorSources::sample_other().into() -} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/mod.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/mod.rs new file mode 100644 index 000000000..073ee4839 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/mod.rs @@ -0,0 +1,7 @@ +mod security_structure_of_factor_instances; +mod security_structure_of_factor_source_ids; +mod security_structure_of_factor_sources; + +pub use security_structure_of_factor_instances::*; +pub use security_structure_of_factor_source_ids::*; +pub use security_structure_of_factor_sources::*; diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_instances.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_instances.rs similarity index 76% rename from crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_instances.rs rename to crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_instances.rs index c6ea1a926..8563e0846 100644 --- a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structure_of_factor_instances.rs +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_instances.rs @@ -1,11 +1,9 @@ use crate::prelude::*; use sargon::SecurityStructureOfFactorInstances as InternalSecurityStructureOfFactorInstances; -decl_matrix_of_factors!( - /// A matrix of FactorInstances - FactorInstance -); - +/// A MatrixOfFactorInstances and an ID which identifies it, this is +/// the Profile data structure representation of the owner key hashes which +/// have been uploaded as Scrypto AccessRules on the AccessController on-ledger. #[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record)] pub struct SecurityStructureOfFactorInstances { /// The ID of the `SecurityStructureOfFactorSourceIDs` in diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_source_ids.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_source_ids.rs new file mode 100644 index 000000000..8ece8ba6f --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_source_ids.rs @@ -0,0 +1,40 @@ +use crate::prelude::*; + +use sargon::SecurityStructureOfFactorSourceIDs as InternalSecurityStructureOfFactorSourceIDs; + +pub type MatrixOfFactorSourceIds = MatrixOfFactorSourceIDs; + +/// A `MatrixOfFactorSourceIDs` and associated metadata, this is +/// the Profile data structure representation of a "SecurityShield". +#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record)] +pub struct SecurityStructureOfFactorSourceIDs { + /// Metadata of this Security Structure, such as globally unique and + /// stable identifier, creation date and user chosen label (name). + pub metadata: SecurityStructureMetadata, + + /// The structure of factors to use for certain roles, Primary, Recovery + /// and Confirmation role. + pub matrix_of_factors: MatrixOfFactorSourceIDs, +} + +delegate_debug_into!( + SecurityStructureOfFactorSourceIDs, + InternalSecurityStructureOfFactorSourceIDs +); + +decl_vec_samples_for!( + SecurityStructuresOfFactorSourceIDs, + SecurityStructureOfFactorSourceIDs +); + +#[uniffi::export] +pub fn new_security_structure_of_factor_source_ids_sample( +) -> SecurityStructureOfFactorSourceIDs { + InternalSecurityStructureOfFactorSourceIDs::sample().into() +} + +#[uniffi::export] +pub fn new_security_structure_of_factor_source_ids_sample_other( +) -> SecurityStructureOfFactorSourceIDs { + InternalSecurityStructureOfFactorSourceIDs::sample_other().into() +} diff --git a/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_sources.rs b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_sources.rs new file mode 100644 index 000000000..7789cc128 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/mfa/security_structures/security_structures/security_structure_of_factor_sources.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; + +use sargon::MatrixOfFactorSources as InternalMatrixOfFactorSources; +use sargon::SecurityStructureOfFactorSources as InternalSecurityStructureOfFactorSources; + +#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record)] +pub struct SecurityStructureOfFactorSources { + /// Metadata of this Security Structure, such as globally unique and + /// stable identifier, creation date and user chosen label (name). + pub metadata: SecurityStructureMetadata, + + /// The structure of factors to use for certain roles, Primary, Recovery + /// and Confirmation role. + pub matrix_of_factors: MatrixOfFactorSources, +} + +#[uniffi::export] +pub fn new_security_structure_of_factor_sources_sample( +) -> SecurityStructureOfFactorSources { + InternalSecurityStructureOfFactorSources::sample().into() +} + +#[uniffi::export] +pub fn new_security_structure_of_factor_sources_sample_other( +) -> SecurityStructureOfFactorSources { + InternalSecurityStructureOfFactorSources::sample_other().into() +} diff --git a/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/badge_virtual_source.rs b/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/badge_virtual_source.rs index af86e8458..a58e7e9d0 100644 --- a/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/badge_virtual_source.rs +++ b/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/badge_virtual_source.rs @@ -7,3 +7,8 @@ pub enum FactorInstanceBadgeVirtualSource { value: HierarchicalDeterministicPublicKey, }, } + +delegate_debug_into!( + FactorInstanceBadgeVirtualSource, + InternalFactorInstanceBadgeVirtualSource +); diff --git a/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/factor_instance_badge.rs b/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/factor_instance_badge.rs index 2ed30c56b..9d2156ad6 100644 --- a/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/factor_instance_badge.rs +++ b/crates/sargon-uniffi/src/profile/v100/factors/factor_instance/factor_instance_badge.rs @@ -13,3 +13,5 @@ pub enum FactorInstanceBadge { value: ResourceAddress, }, } + +delegate_debug_into!(FactorInstanceBadge, InternalFactorInstanceBadge); diff --git a/crates/sargon-uniffi/src/profile/v100/factors/factor_source.rs b/crates/sargon-uniffi/src/profile/v100/factors/factor_source.rs index 0256444e1..2f7e3a222 100644 --- a/crates/sargon-uniffi/src/profile/v100/factors/factor_source.rs +++ b/crates/sargon-uniffi/src/profile/v100/factors/factor_source.rs @@ -36,6 +36,8 @@ pub enum FactorSource { }, } +delegate_debug_into!(FactorSource, InternalFactorSource); + #[uniffi::export] pub fn factor_source_to_string(factor_source: &FactorSource) -> String { factor_source.into_internal().to_string() diff --git a/crates/sargon-uniffi/src/signing/mod.rs b/crates/sargon-uniffi/src/signing/mod.rs index e5edb0e13..e37030828 100644 --- a/crates/sargon-uniffi/src/signing/mod.rs +++ b/crates/sargon-uniffi/src/signing/mod.rs @@ -2,7 +2,6 @@ mod hd_signature; mod hd_signature_input; mod invalid_transaction_if_neglected; mod neglected_factors; -mod role_kind; mod sign_request; mod sign_response; mod sign_with_factors_outcome; @@ -14,7 +13,6 @@ pub use hd_signature::*; pub use hd_signature_input::*; pub use invalid_transaction_if_neglected::*; pub use neglected_factors::*; -pub use role_kind::*; pub use sign_request::*; pub use sign_response::*; pub use sign_with_factors_outcome::*; diff --git a/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_structures.rs b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_structures.rs index 9f04b9d07..156502106 100644 --- a/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_structures.rs +++ b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_structures.rs @@ -56,4 +56,18 @@ impl SargonOS { .await .into_result() } + + /// Returns the status of the prerequisites for building a Security Shield. + /// + /// According to [definition][doc], a Security Shield can be built if the user has, asides from + /// the Identity factor, "2 or more factors, one of which must be Hardware" + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Factor-Prerequisites + pub fn security_shield_prerequisites_status( + &self, + ) -> Result { + self.wrapped + .security_shield_prerequisites_status() + .into_result() + } } diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 71d3a1518..7b01cb8ee 100644 --- a/crates/sargon/Cargo.toml +++ b/crates/sargon/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sargon" # Don't forget to update version in crates/sargon-uniffi/Cargo.toml -version = "1.1.73" +version = "1.1.74" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs b/crates/sargon/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs index ef7ba5633..70ca149f6 100644 --- a/crates/sargon/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs +++ b/crates/sargon/src/factor_instances_provider/provider/factor_instances_provider_unit_tests.rs @@ -462,27 +462,27 @@ async fn cache_is_unchanged_in_case_of_failure() { assert_eq!(all_accounts.len(), 3 * n); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only([bdfs.clone()], 1) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone()], + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 1, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( [bdfs.clone()], - 1, - ) - .unwrap(), - ) - .unwrap(); + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let all_accounts = os .profile() @@ -610,28 +610,31 @@ async fn test_assert_factor_instances_invalid() { .unwrap(); let bdfs = FactorSource::from(os.bdfs().unwrap()); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only([bdfs.clone()], 1) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone()], + + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 1, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( [bdfs.clone()], - 1, - ) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (security_structure_of_fis, _, _) = os.make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome(IndexSet::from_iter([alice.address()]), shield_0.clone()).await.unwrap(); let security_structure_of_fi = @@ -949,30 +952,27 @@ async fn test_securified_accounts() { os.add_factor_source(arculus.clone()).await.unwrap(); os.add_factor_source(password.clone()).await.unwrap(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone(), ledger.clone(), arculus.clone()], + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 2, - ) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ) - .unwrap(); + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone(), arculus.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (security_structures_of_fis, instances_in_cache_consumer, derivation_outcome) = os .make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome( @@ -987,13 +987,13 @@ async fn test_securified_accounts() { "should have used cache" ); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); let alice_sec = security_structures_of_fis.get(&alice.address()).unwrap(); let alice_matrix = alice_sec.matrix_of_factors.clone(); - assert_eq!(alice_matrix.primary_role.threshold, 2); + assert_eq!(alice_matrix.primary().get_threshold(), 2); assert_eq!( alice_matrix @@ -1032,7 +1032,7 @@ async fn test_securified_accounts() { let bob_sec = security_structures_of_fis.get(&bob.address()).unwrap(); let bob_matrix = bob_sec.matrix_of_factors.clone(); - assert_eq!(bob_matrix.primary_role.threshold, 2); + assert_eq!(bob_matrix.primary().get_threshold(), 2); assert_eq!( bob_matrix @@ -1086,21 +1086,27 @@ async fn test_securified_accounts() { "First account created with ledger, should have index 0, even though this ledger was used in the shield, since we are using two different KeySpaces for Securified and Unsecurified accounts." ); - let matrix_1 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - ) - .unwrap(); + let matrix_1 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 1, + [password.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [password.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [password.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_1 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 1").unwrap()), - 14, - matrix_1, - ); + let shield_1 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_1); let (security_structures_of_fis, instances_in_cache_consumer, _) = os .make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome( @@ -1110,13 +1116,13 @@ async fn test_securified_accounts() { .await .unwrap(); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); let carol_sec = security_structures_of_fis.get(&carol.address()).unwrap(); let carol_matrix = carol_sec.matrix_of_factors.clone(); - assert_eq!(carol_matrix.primary_role.get_override_factors().len(), 1); + assert_eq!(carol_matrix.primary_role.get_threshold_factors().len(), 1); assert_eq!( carol_matrix @@ -1151,7 +1157,7 @@ async fn test_securified_accounts() { .await .unwrap(); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); assert!( @@ -1212,28 +1218,30 @@ async fn securify_accounts_when_cache_is_half_full_single_factor_source() { .await .unwrap(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only([bdfs.clone()], 1) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone()], + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 1, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( [bdfs.clone()], - 1, - ) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let profile = os.profile().unwrap(); let all_accounts = profile .accounts_on_all_networks_including_hidden() @@ -1357,31 +1365,30 @@ async fn securify_accounts_when_cache_is_half_full_multiple_factor_sources() { assert_eq!(derivation_outcome.debug_was_derived.len(), 3 * n); // `n` missing + CACHE filling 2*n more. - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone(), ledger.clone(), arculus.clone()], + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 2, - ) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone(), arculus.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let all_accounts = os .profile() .unwrap() @@ -1595,28 +1602,30 @@ async fn securify_personas_when_cache_is_half_full_single_factor_source() { let (_, _) = os.batch_create_many_personas_with_bdfs_with_derivation_outcome_then_save_once(3 * n as u16, NetworkID::Mainnet, "Persona".to_owned()).await.unwrap(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only([bdfs.clone()], 1) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone()], + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 1, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( [bdfs.clone()], - 1, - ) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let all_personas = os .profile() .unwrap() @@ -1726,19 +1735,30 @@ async fn create_single_account() { "should have used cache" ); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([bdfs.clone()]).unwrap(), - RecoveryRoleWithFactorSources::override_only([bdfs.clone()]).unwrap(), - ConfirmationRoleWithFactorSources::override_only([bdfs.clone()]) - .unwrap(), - ) - .unwrap(); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 1, + [bdfs.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (security_structures_of_fis, instances_in_cache_consumer, derivation_outcome) = os .make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome( @@ -1748,7 +1768,7 @@ async fn create_single_account() { .await .unwrap(); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); assert!( @@ -1807,30 +1827,27 @@ async fn securified_personas() { os.add_factor_source(arculus.clone()).await.unwrap(); os.add_factor_source(password.clone()).await.unwrap(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only( - [bdfs.clone(), ledger.clone(), arculus.clone()], + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 2, - ) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], [bdfs.clone(), ledger.clone(), arculus.clone()], - 2, - ) - .unwrap(), - ) - .unwrap(); + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone(), arculus.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (security_structures_of_fis, instances_in_cache_consumer, derivation_outcome) = os .make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome( @@ -1845,13 +1862,13 @@ async fn securified_personas() { "should have used cache" ); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); let batman_sec = security_structures_of_fis.get(&batman.address()).unwrap(); let batman_matrix = batman_sec.matrix_of_factors.clone(); - assert_eq!(batman_matrix.primary_role.threshold, 2); + assert_eq!(batman_matrix.primary().get_threshold(), 2); assert_eq!( batman_matrix @@ -1891,7 +1908,7 @@ async fn securified_personas() { security_structures_of_fis.get(&satoshi.address()).unwrap(); let satoshi_matrix = satoshi_sec.matrix_of_factors.clone(); - assert_eq!(satoshi_matrix.primary_role.threshold, 2); + assert_eq!(satoshi_matrix.primary().get_threshold(), 2); assert_eq!( satoshi_matrix @@ -1945,21 +1962,30 @@ async fn securified_personas() { "First persona created with ledger, should have index 0, even though this ledger was used in the shield, since we are using two different KeySpaces for Securified and Unsecurified personas." ); - let matrix_1 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([password.clone()]) - .unwrap(), - ) - .unwrap(); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_1 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 1, + [password.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [password.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [password.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_1 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 1").unwrap()), - 14, - matrix_1, - ); + let shield_1 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_1); let (security_structures_of_fis, instances_in_cache_consumer, derivation_outcome) = os .make_security_structure_of_factor_instances_for_entities_without_consuming_cache_with_derivation_outcome( @@ -1969,7 +1995,7 @@ async fn securified_personas() { .await .unwrap(); - // dont forget to consume! + // Don't forget to consume! instances_in_cache_consumer.consume().await.unwrap(); assert!( @@ -1980,7 +2006,7 @@ async fn securified_personas() { let hyde_sec = security_structures_of_fis.get(&hyde.address()).unwrap(); let hyde_matrix = hyde_sec.matrix_of_factors.clone(); - assert_eq!(hyde_matrix.primary_role.get_override_factors().len(), 1); + assert_eq!(hyde_matrix.primary_role.get_threshold_factors().len(), 1); assert_eq!( hyde_matrix @@ -2062,12 +2088,12 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { DerivationPreset::all().len() * CACHE_FILLING_QUANTITY ); - let fs_device = FactorSource::from(os.bdfs().unwrap()); - let fs_arculus = FactorSource::sample_arculus(); - let fs_ledger = FactorSource::sample_ledger(); + let bdfs = FactorSource::from(os.bdfs().unwrap()); + let arculus = FactorSource::sample_arculus(); + let ledger = FactorSource::sample_ledger(); - os.add_factor_source(fs_arculus.clone()).await.unwrap(); - os.add_factor_source(fs_ledger.clone()).await.unwrap(); + os.add_factor_source(arculus.clone()).await.unwrap(); + os.add_factor_source(ledger.clone()).await.unwrap(); let factor_source_count = os.profile().unwrap().factor_sources.len(); @@ -2099,22 +2125,30 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { .into_iter() .collect_vec(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([fs_device.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([fs_device.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([fs_device.clone()]) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 1, + [bdfs.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (_, derivation_outcome) = os .__OFFLINE_ONLY_securify_accounts( unnamed_accounts @@ -2146,7 +2180,7 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { let next_index_veci = next_index_profile_assigner .next( - fs_device.id_from_hash(), + bdfs.id_from_hash(), DerivationPreset::AccountVeci .index_agnostic_path_on_network(network), ) @@ -2162,7 +2196,7 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { let next_index_mfa = next_index_profile_assigner .next( - fs_device.id_from_hash(), + bdfs.id_from_hash(), DerivationPreset::AccountMfa .index_agnostic_path_on_network(network), ) @@ -2176,7 +2210,7 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { let (alice, derivation_outcome) = os .create_and_save_new_account_with_factor_with_derivation_outcome( - fs_device.clone(), + bdfs.clone(), network, "Alice", ) @@ -2250,18 +2284,18 @@ async fn securified_all_accounts_next_veci_does_not_start_at_zero() { #[actix_rt::test] async fn securified_accounts_asymmetric_indices() { - let (os, fs_device) = SargonOS::with_bdfs().await; + let (os, bdfs) = SargonOS::with_bdfs().await; let cache = os.cache_snapshot().await; assert_eq!( cache.total_number_of_factor_instances(), DerivationPreset::all().len() * CACHE_FILLING_QUANTITY ); - let fs_arculus = FactorSource::sample_arculus(); - let fs_ledger = FactorSource::sample_ledger(); + let arculus = FactorSource::sample_arculus(); + let ledger = FactorSource::sample_ledger(); - os.add_factor_source(fs_arculus.clone()).await.unwrap(); - os.add_factor_source(fs_ledger.clone()).await.unwrap(); + os.add_factor_source(arculus.clone()).await.unwrap(); + os.add_factor_source(ledger.clone()).await.unwrap(); let number_of_factor_sources = os.profile().unwrap().factor_sources.len(); assert_eq!(number_of_factor_sources, 3); @@ -2282,7 +2316,7 @@ async fn securified_accounts_asymmetric_indices() { // first create CACHE_FILLING_QUANTITY many "unnamed" accounts - let (_, derivation_outcome) = os.batch_create_many_accounts_with_factor_source_with_derivation_outcome_then_save_once(fs_device.clone(), CACHE_FILLING_QUANTITY as u16, network, "Acco".to_owned()).await.unwrap(); + let (_, derivation_outcome) = os.batch_create_many_accounts_with_factor_source_with_derivation_outcome_then_save_once(bdfs.clone(), CACHE_FILLING_QUANTITY as u16, network, "Acco".to_owned()).await.unwrap(); assert!(derivation_outcome.debug_was_derived.is_empty()); let unnamed_accounts = os @@ -2292,31 +2326,30 @@ async fn securified_accounts_asymmetric_indices() { .into_iter() .collect_vec(); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::threshold_factors_only( - [fs_device.clone()], + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_0 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( 1, - ) - .unwrap(), - RecoveryRoleWithFactorSources::threshold_factors_only( - [fs_device.clone()], - 1, - ) - .unwrap(), - ConfirmationRoleWithFactorSources::threshold_factors_only( - [fs_device.clone()], - 1, - ) - .unwrap(), - ) - .unwrap(); - - let shield_0 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 0").unwrap()), - 14, - matrix_0, - ); + [bdfs.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; + let shield_0 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_0); let (_, derivation_outcome) = os .__OFFLINE_ONLY_securify_accounts( unnamed_accounts @@ -2336,7 +2369,7 @@ async fn securified_accounts_asymmetric_indices() { let (alice, derivation_outcome) = os .create_and_save_new_account_with_factor_with_derivation_outcome( - fs_device.clone(), + bdfs.clone(), network, "Alice", ) @@ -2357,7 +2390,7 @@ async fn securified_accounts_asymmetric_indices() { let (bob, _) = os .create_and_save_new_account_with_factor_with_derivation_outcome( - fs_device.clone(), + bdfs.clone(), network, "Bob", ) @@ -2366,7 +2399,7 @@ async fn securified_accounts_asymmetric_indices() { let (carol, _) = os .create_and_save_new_account_with_factor_with_derivation_outcome( - fs_device.clone(), + bdfs.clone(), network, "Carol", ) @@ -2375,7 +2408,7 @@ async fn securified_accounts_asymmetric_indices() { let (diana, _) = os .create_and_save_new_account_with_factor_with_derivation_outcome( - fs_device.clone(), + bdfs.clone(), network, "Diana", ) @@ -2406,30 +2439,30 @@ async fn securified_accounts_asymmetric_indices() { 4 ); - let matrix_1 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - ]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - ]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - ]) - .unwrap(), - ) - .unwrap(); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_1 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 2, + [bdfs.clone(), arculus.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), arculus.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), arculus.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_1 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 1").unwrap()), - 14, - matrix_1, - ); + let shield_1 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_1); let (securified_alice, derivation_outcome) = os .__OFFLINE_ONLY_securify_account(alice.address(), &shield_1) @@ -2457,12 +2490,12 @@ async fn securified_accounts_asymmetric_indices() { .collect::>(), [ ( - fs_device.id_from_hash(), + bdfs.id_from_hash(), HDPathComponent::from_local_key_space(30, KeySpace::Securified) .unwrap() ), ( - fs_arculus.id_from_hash(), + arculus.id_from_hash(), HDPathComponent::from_local_key_space(0, KeySpace::Securified) .unwrap() ), @@ -2471,30 +2504,30 @@ async fn securified_accounts_asymmetric_indices() { .collect::>() ); - let matrix_2 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - ) - .unwrap(); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_2 = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 2, + [bdfs.clone(), ledger.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; - let shield_2 = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 2").unwrap()), - 14, - matrix_2, - ); + let shield_2 = + SecurityStructureOfFactorSources::new(DisplayName::sample(), matrix_2); let (securified_bob, derivation_outcome) = os .__OFFLINE_ONLY_securify_account(bob.address(), &shield_2) @@ -2522,12 +2555,12 @@ async fn securified_accounts_asymmetric_indices() { .collect::>(), [ ( - fs_device.id_from_hash(), + bdfs.id_from_hash(), HDPathComponent::from_local_key_space(31, KeySpace::Securified) // Alice used 30 .unwrap() ), ( - fs_ledger.id_from_hash(), + ledger.id_from_hash(), HDPathComponent::from_local_key_space(0, KeySpace::Securified) .unwrap() ), @@ -2561,12 +2594,12 @@ async fn securified_accounts_asymmetric_indices() { .collect::>(), [ ( - fs_device.id_from_hash(), + bdfs.id_from_hash(), HDPathComponent::from_local_key_space(32, KeySpace::Securified) // Alice used 30, Bob used 31 .unwrap() ), ( - fs_arculus.id_from_hash(), + arculus.id_from_hash(), HDPathComponent::from_local_key_space(1, KeySpace::Securified) // Alice used 0 .unwrap() ), @@ -2578,31 +2611,30 @@ async fn securified_accounts_asymmetric_indices() { // CLEAR CACHE os.clear_cache().await; - let matrix_3fa = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([ - fs_device.clone(), - fs_arculus.clone(), - fs_ledger.clone(), - ]) - .unwrap(), - ) - .unwrap(); + // This is NOT a valid Matrix! But for the purpose of this test, it's fine. + // We are not testing valid matrices here... we are testing the factor + // instances provider... + let matrix_3fa = MatrixOfFactorSources { + primary_role: PrimaryRoleWithFactorSources::with_factors( + 2, + [bdfs.clone(), ledger.clone(), arculus.clone()], + [], + ), + recovery_role: RecoveryRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone(), arculus.clone()], + ), + confirmation_role: ConfirmationRoleWithFactorSources::with_factors( + 0, + [], + [bdfs.clone(), ledger.clone(), arculus.clone()], + ), + number_of_days_until_auto_confirm: 1, + }; let shield_3fa = SecurityStructureOfFactorSources::new( - SecurityStructureMetadata::new(DisplayName::new("Shield 3fa").unwrap()), - 14, + DisplayName::sample(), matrix_3fa, ); @@ -2632,7 +2664,7 @@ async fn securified_accounts_asymmetric_indices() { .collect::>(), [ ( - fs_device.id_from_hash(), + bdfs.id_from_hash(), HDPathComponent::from_local_key_space( diana_mfa_device, KeySpace::Securified @@ -2640,7 +2672,7 @@ async fn securified_accounts_asymmetric_indices() { .unwrap() ), ( - fs_arculus.id_from_hash(), + arculus.id_from_hash(), HDPathComponent::from_local_key_space( diana_mfa_arculus, KeySpace::Securified @@ -2648,7 +2680,7 @@ async fn securified_accounts_asymmetric_indices() { .unwrap() ), ( - fs_ledger.id_from_hash(), + ledger.id_from_hash(), HDPathComponent::from_local_key_space( diana_mfa_ledger, KeySpace::Securified @@ -2706,21 +2738,21 @@ async fn securified_accounts_asymmetric_indices() { .collect::>(), [ ( - fs_device.id_from_hash(), + bdfs.id_from_hash(), HDPathComponent::Securified( SecurifiedU30::try_from(diana_mfa_device + offset) .unwrap() ) ), ( - fs_arculus.id_from_hash(), + arculus.id_from_hash(), HDPathComponent::Securified( SecurifiedU30::try_from(diana_mfa_arculus + offset) .unwrap() ) ), ( - fs_ledger.id_from_hash(), + ledger.id_from_hash(), HDPathComponent::Securified( SecurifiedU30::try_from(diana_mfa_ledger + offset) .unwrap() diff --git a/crates/sargon/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs b/crates/sargon/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs index 1af9895fa..54c65fd70 100644 --- a/crates/sargon/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs +++ b/crates/sargon/src/factor_instances_provider/provider/provider_adopters/securify_entity_factor_instances_provider.rs @@ -158,15 +158,7 @@ mod tests { let _ = SUT::for_account_mfa( Arc::new(cache_client), Arc::new(Profile::sample_from([fs.clone()], [&a], [])), - MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ) - .unwrap(), + MatrixOfFactorSources::sample(), IndexSet::::new(), // <---- EMPTY => should_panic Arc::new(TestDerivationInteractor::default()), ) @@ -180,19 +172,10 @@ mod tests { let fs = FactorSource::sample_at(0); let a = Account::sample(); let cache_client = FactorInstancesCacheClient::in_memory(); - let _ = SUT::for_account_mfa( Arc::new(cache_client), Arc::new(Profile::sample_from([fs.clone()], [&a], [])), - MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ) - .unwrap(), + MatrixOfFactorSources::sample(), IndexSet::just(Account::sample_other().address()), // <---- unknown => should_panic Arc::new(TestDerivationInteractor::default()), ) @@ -225,15 +208,7 @@ mod tests { let _ = SUT::for_account_mfa( Arc::new(cache_client), Arc::new(profile), - MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ) - .unwrap(), + MatrixOfFactorSources::sample(), IndexSet::from_iter([mainnet_account.address()]), Arc::new(TestDerivationInteractor::default()), ) @@ -279,15 +254,7 @@ mod tests { let _ = SUT::for_account_mfa( Arc::new(cache_client), Arc::new(profile), - MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([fs.clone()]) - .unwrap(), - ) - .unwrap(), + MatrixOfFactorSources::sample(), IndexSet::from_iter([ mainnet_account.address(), stokenet_account.address(), @@ -322,15 +289,19 @@ mod tests { .unwrap(); assert!(derivation_outcome.debug_was_derived.is_empty()); - let matrix_0 = MatrixOfFactorSources::new( - PrimaryRoleWithFactorSources::override_only([bdfs.clone()]) - .unwrap(), - RecoveryRoleWithFactorSources::override_only([bdfs.clone()]) - .unwrap(), - ConfirmationRoleWithFactorSources::override_only([bdfs.clone()]) - .unwrap(), - ) - .unwrap(); + os.add_factor_source(FactorSource::sample_ledger()) + .await + .unwrap(); + os.add_factor_source(FactorSource::sample_password()) + .await + .unwrap(); + let factor_sources = &os.profile().unwrap().factor_sources; + let matrix_ids = MatrixTemplate::config_1_4() + .materialize(factor_sources.items()) + .unwrap(); + + let matrix_0 = + MatrixOfFactorSources::new(matrix_ids, factor_sources).unwrap(); let cache_client = Arc::new(os.clients.factor_instances_cache.clone()); let profile = Arc::new(os.profile().unwrap()); diff --git a/crates/sargon/src/hierarchical_deterministic/cap26/paths/account_path.rs b/crates/sargon/src/hierarchical_deterministic/cap26/paths/account_path.rs index 3cd446ae1..98b55b9b1 100644 --- a/crates/sargon/src/hierarchical_deterministic/cap26/paths/account_path.rs +++ b/crates/sargon/src/hierarchical_deterministic/cap26/paths/account_path.rs @@ -12,7 +12,7 @@ use crate::prelude::*; /// m / purpose' / coin_type' / network' / entity_kind' / key_kind' / entity_index' /// ``` /// -/// The `AccountPath` struct is parametrized by Radix network id and account index, but fixes the other +/// The `AccountPath` struct is parameterized by Radix network id and account index, but fixes the other /// constants in the path as follows: /// /// ```text diff --git a/crates/sargon/src/hierarchical_deterministic/cap26/paths/identity_path.rs b/crates/sargon/src/hierarchical_deterministic/cap26/paths/identity_path.rs index 671e80929..9cb485029 100644 --- a/crates/sargon/src/hierarchical_deterministic/cap26/paths/identity_path.rs +++ b/crates/sargon/src/hierarchical_deterministic/cap26/paths/identity_path.rs @@ -12,7 +12,7 @@ use crate::prelude::*; /// m / purpose' / coin_type' / network' / entity_kind' / key_kind' / entity_index' /// ``` /// -/// The `IdentityPath` struct is parametrized by Radix network id and entity index, but fixes the other +/// The `IdentityPath` struct is parameterized by Radix network id and entity index, but fixes the other /// constants in the path as follows: /// /// ```text diff --git a/crates/sargon/src/lib.rs b/crates/sargon/src/lib.rs index 68d5698ea..0af2321b1 100644 --- a/crates/sargon/src/lib.rs +++ b/crates/sargon/src/lib.rs @@ -5,6 +5,8 @@ #![allow(internal_features)] #![feature(iter_repeat_n)] #![feature(future_join)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] #![feature(trait_upcasting)] mod core; @@ -34,10 +36,10 @@ pub mod prelude { pub use crate::system::*; pub use crate::types::*; pub use crate::wrapped_radix_engine_toolkit::*; - pub use radix_rust::prelude::{ indexmap, BTreeSet, HashMap, HashSet, IndexMap, IndexSet, }; + pub(crate) use std::marker::PhantomData; pub(crate) use ::hex::decode as hex_decode; pub(crate) use ::hex::encode as hex_encode; diff --git a/crates/sargon/src/profile/logic/account/query_security_structures.rs b/crates/sargon/src/profile/logic/account/query_security_structures.rs index 8de8e671a..5b9885408 100644 --- a/crates/sargon/src/profile/logic/account/query_security_structures.rs +++ b/crates/sargon/src/profile/logic/account/query_security_structures.rs @@ -1,5 +1,23 @@ use crate::prelude::*; +decl_identified_vec_of!( + /// A collection of [`SecurityStructureOfFactorSources`] + SecurityStructuresOfFactorSources, + SecurityStructureOfFactorSources +); + +impl HasSampleValues for SecurityStructuresOfFactorSources { + fn sample() -> Self { + Self::from_iter([ + SecurityStructureOfFactorSources::sample(), + SecurityStructureOfFactorSources::sample_other(), + ]) + } + fn sample_other() -> Self { + Self::from_iter([SecurityStructureOfFactorSources::sample_other()]) + } +} + impl Profile { /// Returns all the SecurityStructuresOfFactorSources, /// by trying to map FactorSourceID level -> FactorSource Level @@ -19,3 +37,106 @@ impl Profile { .collect::>() } } + +impl Profile { + /// Returns the status of the prerequisites for building a Security Shield. + /// + /// According to [definition][doc], a Security Shield can be built if the user has, asides from + /// the Identity factor, "2 or more factors, one of which must be Hardware" + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Factor-Prerequisites + pub fn security_shield_prerequisites_status( + &self, + ) -> SecurityShieldPrerequisitesStatus { + let factor_sources = self.factor_sources.clone(); + let count_excluding_identity = factor_sources + .iter() + .filter(|f| f.category() != FactorSourceCategory::Identity) + .count(); + let count_hardware = factor_sources + .iter() + .filter(|f| f.category() == FactorSourceCategory::Hardware) + .count(); + if count_hardware < 1 { + SecurityShieldPrerequisitesStatus::HardwareRequired + } else if count_excluding_identity < 2 { + SecurityShieldPrerequisitesStatus::AnyRequired + } else { + SecurityShieldPrerequisitesStatus::Sufficient + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = Profile; + + #[test] + fn security_shield_prerequisites_status_hardware_required() { + let mut sut = SUT::sample(); + + // Test the case where user doesn't have any factors + sut.factor_sources = FactorSources::from_iter([]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::HardwareRequired); + + // Test the case where the user has identity factor + sut.factor_sources = + FactorSources::from_iter([FactorSource::sample_device()]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::HardwareRequired); + + // Test the case where the user also has other non-hardware factors + sut.factor_sources = FactorSources::from_iter([ + FactorSource::sample_device(), + FactorSource::sample_password(), + FactorSource::sample_trusted_contact_frank(), + FactorSource::sample_off_device(), + ]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::HardwareRequired); + } + + #[test] + fn security_shield_prerequisites_status_any_required() { + let mut sut = SUT::sample(); + + // Test the case where user only has hardware factor + sut.factor_sources = + FactorSources::from_iter([FactorSource::sample_arculus()]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::AnyRequired); + + // Test the case where the user also has identity factors + sut.factor_sources = FactorSources::from_iter([ + FactorSource::sample_arculus(), + FactorSource::sample_device(), + ]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::AnyRequired); + } + + #[test] + fn security_shield_prerequisites_status_sufficient() { + let mut sut = SUT::sample(); + + // Test the case where user only has hardware factors + sut.factor_sources = FactorSources::from_iter([ + FactorSource::sample_arculus(), + FactorSource::sample_ledger(), + ]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::Sufficient); + + // Test the case where the user has 1 hardware factor and 1 non-hardware factor + sut.factor_sources = FactorSources::from_iter([ + FactorSource::sample_ledger(), + FactorSource::sample_password(), + ]); + let result = sut.security_shield_prerequisites_status(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::Sufficient); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs b/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs deleted file mode 100644 index 1fd6a7483..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/decl_security_structure_of.rs +++ /dev/null @@ -1,378 +0,0 @@ -use crate::prelude::*; - -macro_rules! decl_role_with_factors_additional_impl { - ( - $role: ident, - FactorInstance - ) => { - paste! { - impl From<[< $role RoleWithFactorInstance s >]> for ScryptoAccessRule { - fn from(value: [< $role RoleWithFactorInstance s >]) -> Self { - ScryptoAccessRule::Protected(ScryptoCompositeRequirement::AnyOf(vec![ - ScryptoCompositeRequirement::BasicRequirement(ScryptoBasicRequirement::CountOf( - value.threshold, - value - .threshold_factors - .into_iter() - .map(|instance| instance.badge) - .map(ScryptoResourceOrNonFungible::from) - .collect(), - )), - ScryptoCompositeRequirement::BasicRequirement(ScryptoBasicRequirement::AnyOf( - value - .override_factors - .into_iter() - .map(|instance| instance.badge) - .map(ScryptoResourceOrNonFungible::from) - .collect(), - )), - ])) - } - } - } - }; - ( - $role: ident, - $factor: ident - ) => {} -} -pub(crate) use decl_role_with_factors_additional_impl; - -macro_rules! decl_role_with_factors_with_role_kind_attrs { - ( - $( - #[doc = $expr: expr] - )* - $role: ident, - $factor: ident, - $($extra_field_name:ident: $extra_field_type:ty,)* - ) => { - paste! { - $( - #[doc = $expr] - )* - #[derive( - Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, - )] - #[serde(rename_all = "camelCase")] - pub struct [< $role RoleWith $factor s >] { - - /// Factors which are used in combination with other instances, amounting to at - /// least `threshold` many instances to perform some function with this role. - /// - /// # Implementation - /// Must allow duplicates, thus using `Vec` since at FactorSourceKind level - /// we might wanna use duplicates, allowing us to build a "template" - /// structure where a role might contain two `FactorSourceKind::TrustedContact`, - /// meaning an instance of this template at FactorSource level - /// (`SecurityStructureOfFactorSources`) will contain two different - /// `TrustedContactFactorSource`s. - pub threshold_factors: Vec<$factor>, - - /// How many threshold factors that must be used to perform some function with this role. - pub threshold: u8, - - /// Overriding / Super admin / "sudo" / God / factors, **ANY** - /// single of these factor which can perform the function of this role, - /// disregarding of `threshold`. - pub override_factors: Vec<$factor>, - - $(pub $extra_field_name: $extra_field_type,)* - } - - - impl RoleWithFactors<$factor> for [< $role RoleWith $factor s >] { - - fn get_threshold_factors(&self) -> &Vec<$factor> { - &self.threshold_factors - } - - fn get_threshold(&self) -> u8 { - self.threshold - } - - fn get_override_factors(&self) -> &Vec<$factor> { - &self.override_factors - } - } - - impl [< $role RoleWith $factor s >] { - - pub fn unique_factors(&self) -> IndexSet<$factor> { - self.all_factors().into_iter().map(|x| x.clone()).collect() - } - - // # Panics - /// Panics if `threshold > threshold_factor.len()` - /// - /// Panics if the same factor is present in both lists - /// - /// Panics if Factor elements are FactorInstances and the derivation - /// path contains a non-securified last path component. - pub fn with_factors_and_role( - $($extra_field_name: $extra_field_type,)* - threshold_factors: impl IntoIterator, - threshold: u8, - override_factors: impl IntoIterator, - ) -> Result { - - let assert_is_securified = |factors: &Vec::<$factor>| -> Result<()> { - let trait_objects: Vec<&dyn IsMaybeKeySpaceAware> = factors.iter().map(|x| x as &dyn IsMaybeKeySpaceAware).collect(); - if trait_objects.iter() - .filter_map(|x| x.maybe_key_space()) - .any(|x| x != KeySpace::Securified) { - return Err(crate::CommonError::IndexUnsecurifiedExpectedSecurified) - } - Ok(()) - }; - - - let threshold_factors = threshold_factors.into_iter().collect_vec(); - - if threshold_factors.len() < threshold as usize { - return Err(CommonError::InvalidSecurityStructureThresholdExceedsFactors { - threshold, - factors: threshold_factors.len() as u8 - }) - } - - let override_factors = override_factors.into_iter().collect_vec(); - - assert_is_securified(&threshold_factors)?; - assert_is_securified(&override_factors)?; - - if !HashSet::<$factor>::from_iter(threshold_factors.clone()) - .intersection(&HashSet::<$factor>::from_iter(override_factors.clone())) - .collect_vec() - .is_empty() { - return Err(CommonError::InvalidSecurityStructureFactorInBothThresholdAndOverride) - } - - Ok(Self { - threshold_factors, - threshold, - override_factors, - $($extra_field_name,)* - }) - } - } - } - }; -} - -pub(crate) use decl_role_with_factors_with_role_kind_attrs; - -macro_rules! decl_role_with_factors { - ( - $( - #[doc = $expr: expr] - )* - $role: ident, - $factor: ident - ) => { - - decl_role_with_factors_with_role_kind_attrs!( - $( - #[doc = $expr] - )* - $role, - $factor, - ); - - paste! { - - impl [< $role RoleWith $factor s >] { - - pub fn new( - threshold_factors: impl IntoIterator, - threshold: u8, - override_factors: impl IntoIterator - ) -> Result { - Self::with_factors_and_role(threshold_factors, threshold, override_factors) - } - - - /// # Panics - /// Panics if `threshold > factors.len()` - /// - /// Panics if Factor elements are FactorInstances and the derivation - /// path contains a non-securified last path component. - pub fn threshold_factors_only( - factors: impl IntoIterator, - threshold: u8, - ) -> Result { - Self::new(factors, threshold, []) - } - - /// # Panics - /// Panics if Factor elements are FactorInstances and the derivation - /// path contains a non-securified last path component. - pub fn override_only( - factors: impl IntoIterator, - ) -> Result { - Self::new([], 0, factors) - } - } - } - - decl_role_with_factors_additional_impl!($role, $factor); - }; -} - -pub(crate) use decl_role_with_factors; - -macro_rules! decl_role_runtime_kind_with_factors { - ( - $( - #[doc = $expr: expr] - )* - $role: ident, - $factor: ident - ) => { - decl_role_with_factors_with_role_kind_attrs!( - $( - #[doc = $expr] - )* - $role, - $factor, - role: RoleKind, - ); - }; -} - -pub(crate) use decl_role_runtime_kind_with_factors; - -macro_rules! decl_matrix_of_factors { - ( - $( - #[doc = $expr: expr] - )* - $factor: ident - ) => { - paste! { - - decl_role_with_factors!( - /// PrimaryRole is used for Signing Transactions. - Primary, - $factor - ); - - decl_role_with_factors!( - /// RecoveryRole is used to recover lost access to an entity. - Recovery, - $factor - ); - - decl_role_with_factors!( - /// ConfirmationRole is used to confirm recovery. - Confirmation, - $factor - ); - - $( - #[doc = $expr] - )* - #[derive( - Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, - )] - #[serde(rename_all = "camelCase")] - pub struct [< MatrixOf $factor s >] { - - /// Used for Signing transactions - pub primary_role: [< PrimaryRoleWith $factor s >], - - /// Used to initiate recovery - resetting the used Security Shield - /// of an entity. - pub recovery_role: [< RecoveryRoleWith $factor s >], - - /// To confirm recovery. - pub confirmation_role: [< ConfirmationRoleWith $factor s >], - } - - impl [< MatrixOf $factor s >] { - pub fn new( - primary_role: [< PrimaryRoleWith $factor s >], - recovery_role: [< RecoveryRoleWith $factor s >], - confirmation_role: [< ConfirmationRoleWith $factor s >], - ) -> Result { - Ok(Self { - primary_role, - recovery_role, - confirmation_role, - }) - } - - pub fn all_factors(&self) -> HashSet<&$factor> { - let mut factors = HashSet::new(); - factors.extend(self.primary_role.all_factors()); - factors.extend(self.recovery_role.all_factors()); - factors.extend(self.confirmation_role.all_factors()); - factors - } - - pub fn get_role_of_kind(&self, role_kind: RoleKind) -> &dyn RoleWithFactors<$factor> { - match role_kind { - RoleKind::Confirmation => &self.confirmation_role, - RoleKind::Primary => &self.primary_role, - RoleKind::Recovery => &self.recovery_role, - } - } - } - } - }; -} - -pub(crate) use decl_matrix_of_factors; - -macro_rules! decl_security_structure_of { - ( - $( - #[doc = $expr: expr] - )* - $factor: ident, - ) => { - - decl_matrix_of_factors!($factor); - - paste! { - - $( - #[doc = $expr] - )* - #[derive( - Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, - )] - #[serde(rename_all = "camelCase")] - pub struct [< SecurityStructureOf $factor s >] { - /// Metadata of this Security Structure, such as globally unique and - /// stable identifier, creation date and user chosen label (name). - pub metadata: SecurityStructureMetadata, - - /// The amount of time until Confirmation Role is automatically - /// exercised, inputted by user in Days in UI, but translate it into - /// epochs ("block time"). - pub number_of_epochs_until_auto_confirmation: u64, - - /// The structure of factors to use for certain roles, Primary, Recovery - /// and Confirmation role. - pub matrix_of_factors: [< MatrixOf $factor s >], - } - - impl [< SecurityStructureOf $factor s >] { - pub fn new(metadata: SecurityStructureMetadata, number_of_epochs_until_auto_confirmation: u64, matrix_of_factors: [< MatrixOf $factor s >]) -> Self { - Self { - metadata, - number_of_epochs_until_auto_confirmation, - matrix_of_factors - } - } - - pub fn all_factors(&self) -> HashSet<&$factor> { - self.matrix_of_factors.all_factors() - } - } - } - }; -} - -pub(crate) use decl_security_structure_of; diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/confirmation_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/confirmation_role_with_factor_instances.rs deleted file mode 100644 index 9b31cf218..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/confirmation_role_with_factor_instances.rs +++ /dev/null @@ -1,48 +0,0 @@ -use crate::prelude::*; - -impl HasRoleKind for ConfirmationRoleWithFactorInstances { - fn role_kind() -> RoleKind { - RoleKind::Confirmation - } -} - -impl HasFactorInstances for ConfirmationRoleWithFactorInstances { - fn unique_factor_instances(&self) -> IndexSet { - self.unique_factors() - } -} - -impl HasSampleValues for ConfirmationRoleWithFactorInstances { - fn sample() -> Self { - Self::new([HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(27).into()], 1, [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(13).into()]) - .unwrap() - } - - fn sample_other() -> Self { - Self::new( - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(6).into(), HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(42).into()], - 2, - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(19).into()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = ConfirmationRoleWithFactorInstances; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/matrix_of_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/matrix_of_factor_instances.rs deleted file mode 100644 index f00f61ba8..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/matrix_of_factor_instances.rs +++ /dev/null @@ -1,202 +0,0 @@ -use sbor::prelude::indexmap::IndexSet; - -use crate::prelude::*; - -impl HasFactorInstances for MatrixOfFactorInstances { - fn unique_factor_instances(&self) -> IndexSet { - let mut set = IndexSet::new(); - set.extend(self.primary_role.all_factors().into_iter().cloned()); - set.extend(self.recovery_role.all_factors().into_iter().cloned()); - set.extend(self.confirmation_role.all_factors().into_iter().cloned()); - set - } -} - -impl HasSampleValues for MatrixOfFactorInstances { - fn sample() -> Self { - Self::new( - PrimaryRoleWithFactorInstances::sample(), - RecoveryRoleWithFactorInstances::sample(), - ConfirmationRoleWithFactorInstances::sample(), - ) - .unwrap() - } - - fn sample_other() -> Self { - Self::new( - PrimaryRoleWithFactorInstances::sample_other(), - RecoveryRoleWithFactorInstances::sample_other(), - ConfirmationRoleWithFactorInstances::sample_other(), - ) - .unwrap() - } -} - -impl MatrixOfFactorInstances { - /// Maps `MatrixOfFactorSources -> MatrixOfFactorInstances` by - /// "assigning" FactorInstances to each MatrixOfFactorInstances from - /// `consuming_instances`. - /// - /// NOTE: - /// **One FactorInstance might be used multiple times in the MatrixOfFactorInstances, - /// e.g. ones in the PrimaryRole(WithFactorInstances) and again in RecoveryRole(WithFactorInstances) or - /// in RecoveryRole(WithFactorInstances)**. - /// - /// However, the same FactorInstance is NEVER used in two different MatrixOfFactorInstances. - /// - /// - pub fn fulfilling_matrix_of_factor_sources_with_instances( - consuming_instances: &mut IndexMap< - FactorSourceIDFromHash, - FactorInstances, - >, - matrix_of_factor_sources: MatrixOfFactorSources, - ) -> Result { - let instances = &consuming_instances.clone(); - - let primary = fulfilling_role_of_factor_sources_with_factor_instances( - instances, - &matrix_of_factor_sources, - PrimaryRoleWithFactorInstances::new, - )?; - let recovery = fulfilling_role_of_factor_sources_with_factor_instances( - instances, - &matrix_of_factor_sources, - RecoveryRoleWithFactorInstances::new, - )?; - let confirmation = - fulfilling_role_of_factor_sources_with_factor_instances( - instances, - &matrix_of_factor_sources, - ConfirmationRoleWithFactorInstances::new, - )?; - - let matrix = Self::new(primary, recovery, confirmation)?; - - // Now that we have assigned instances, **possibly the SAME INSTANCE to multiple roles**, - // lets delete them from the `consuming_instances` map. - for instance in matrix.all_factors() { - let fsid = - &FactorSourceIDFromHash::try_from(instance.factor_source_id) - .unwrap(); - let existing = consuming_instances.get_mut(fsid).unwrap(); - - let to_remove = HierarchicalDeterministicFactorInstance::try_from( - instance.clone(), - ) - .unwrap(); - - // We remove at the beginning of the list first. - existing.shift_remove(&to_remove); - - if existing.is_empty() { - // not needed per se, but feels prudent to "prune". - consuming_instances.shift_remove_entry(fsid); - } - } - - Ok(matrix) - } -} - -// TODO: MFA - Upgrade this method to follow the rules of when a factor instance might -// be used by MULTIPLE roles. This is a temporary solution to get the tests to pass. -// A proper solution should use follow the rules laid out in: -// https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields -fn fulfilling_role_of_factor_sources_with_factor_instances< - U: HasRoleKind + RoleWithFactors, ->( - consuming_instances: &IndexMap, - matrix_of_factor_sources: &MatrixOfFactorSources, - make_role: impl FnOnce( - Vec, - u8, - Vec, - ) -> Result, -) -> Result { - let role_kind = U::role_kind(); - let role_of_sources = matrix_of_factor_sources.get_role_of_kind(role_kind); - let threshold: u8 = role_of_sources.get_threshold(); - - // Threshold factors - let ti = - try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( - consuming_instances, - role_of_sources.get_threshold_factors() - )?; - - // Override factors - let oi = - try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( - consuming_instances, - role_of_sources.get_override_factors() - )?; - - let role = make_role(ti, threshold, oi)?; - - assert_eq!(role.get_role_kind(), role_kind); - Ok(role) -} - -fn try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( - instances: &IndexMap, - from: &[FactorSource], -) -> Result> { - from.iter() - .map(|f| { - if let Some(existing) = instances.get(&f.id_from_hash()) { - let hd_instance = existing.first().ok_or( - CommonError::MissingFactorMappingInstancesIntoRole, - )?; - let instance = FactorInstance::from(hd_instance); - Ok(instance) - } else { - Err(CommonError::MissingFactorMappingInstancesIntoRole) - } - }) - .collect::>>() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = MatrixOfFactorInstances; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } - - #[test] - fn err_if_no_instance_found_for_factor_source() { - assert!(matches!( - SUT::fulfilling_matrix_of_factor_sources_with_instances( - &mut IndexMap::new(), - MatrixOfFactorSources::sample() - ), - Err(CommonError::MissingFactorMappingInstancesIntoRole) - )); - } - - #[test] - fn err_if_empty_instance_found_for_factor_source() { - assert!(matches!( - SUT::fulfilling_matrix_of_factor_sources_with_instances( - &mut IndexMap::kv( - FactorSource::sample_device_babylon().id_from_hash(), - FactorInstances::from_iter([]) - ), - MatrixOfFactorSources::sample() - ), - Err(CommonError::MissingFactorMappingInstancesIntoRole) - )); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/primary_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/primary_role_with_factor_instances.rs deleted file mode 100644 index 304760d0c..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/primary_role_with_factor_instances.rs +++ /dev/null @@ -1,63 +0,0 @@ -use crate::prelude::*; - -impl HasRoleKind for PrimaryRoleWithFactorInstances { - fn role_kind() -> RoleKind { - RoleKind::Primary - } -} - -impl HasFactorInstances for PrimaryRoleWithFactorInstances { - fn unique_factor_instances(&self) -> IndexSet { - self.unique_factors() - } -} - -impl HasSampleValues for PrimaryRoleWithFactorInstances { - fn sample() -> Self { - Self::new([HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0).into()], 1, [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(0).into()]) - .unwrap() - } - - fn sample_other() -> Self { - Self::new( - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(1).into(), - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(11).into(), - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(12).into()], - 2, - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(6).into()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = PrimaryRoleWithFactorInstances; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } - - #[test] - fn primary_role_non_securified_threshold_instances_is_err() { - assert!(matches!( - SUT::threshold_factors_only( - [ - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_unsecurified_at_index(0).into() - ], - 1, - ), - Err(CommonError::IndexUnsecurifiedExpectedSecurified) - )); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/recovery_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/recovery_role_with_factor_instances.rs deleted file mode 100644 index 01d195d03..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/recovery_role_with_factor_instances.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::prelude::*; - -impl HasRoleKind for RecoveryRoleWithFactorInstances { - fn role_kind() -> RoleKind { - RoleKind::Recovery - } -} - -impl HasFactorInstances for RecoveryRoleWithFactorInstances { - fn unique_factor_instances(&self) -> IndexSet { - self.unique_factors() - } -} - -impl HasSampleValues for RecoveryRoleWithFactorInstances { - fn sample() -> Self { - Self::new([HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(54).into()], 1, [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(237).into()]) - .unwrap() - } - - fn sample_other() -> Self { - Self::new( - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(65).into(), - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(25).into()], - 2, - [HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_securified_at_index(31).into()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = RecoveryRoleWithFactorInstances; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/security_structure_of_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/security_structure_of_factor_instances.rs deleted file mode 100644 index 0ec4b1a80..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/security_structure_of_factor_instances.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::prelude::*; - -decl_matrix_of_factors!( - /// A matrix of FactorInstances - FactorInstance -); - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] -#[serde(rename_all = "camelCase")] -pub struct SecurityStructureOfFactorInstances { - /// The ID of the `SecurityStructureOfFactorSourceIDs` in - /// `profile.app_preferences.security.security_structures_of_factor_source_ids` - /// which was used to derive the factor instances in this structure. Or rather: - /// The id of `SecurityStructureOfFactorSources`. - pub security_structure_id: SecurityStructureID, - - /// The structure of factors to use for certain roles, Primary, Recovery - /// and Confirmation role. - pub matrix_of_factors: MatrixOfFactorInstances, -} - -impl SecurityStructureOfFactorInstances { - pub fn new( - security_structure_id: SecurityStructureID, - matrix_of_factors: MatrixOfFactorInstances, - ) -> Self { - Self { - security_structure_id, - matrix_of_factors, - } - } -} - -impl Identifiable for SecurityStructureOfFactorInstances { - type ID = ::ID; - - fn id(&self) -> Self::ID { - self.security_structure_id - } -} - -impl HasSampleValues for SecurityStructureOfFactorInstances { - fn sample() -> Self { - Self { - security_structure_id: SecurityStructureID::sample(), - matrix_of_factors: MatrixOfFactorInstances::sample(), - } - } - - fn sample_other() -> Self { - Self { - security_structure_id: SecurityStructureID::sample_other(), - matrix_of_factors: MatrixOfFactorInstances::sample_other(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = SecurityStructureOfFactorInstances; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/confirmation_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/confirmation_role_with_factor_source_ids.rs deleted file mode 100644 index 7b8e4d912..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/confirmation_role_with_factor_source_ids.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::prelude::*; - -impl From - for ConfirmationRoleWithFactorSourceIDs -{ - fn from(value: ConfirmationRoleWithFactorSources) -> Self { - Self::new( - value.threshold_factors.iter().map(|x| x.factor_source_id()), - value.threshold, - value.override_factors.iter().map(|x| x.factor_source_id()), - ) - .expect("ConfirmationRoleWithFactorSources has already been validated.") - } -} - -impl HasSampleValues for ConfirmationRoleWithFactorSourceIDs { - fn sample() -> Self { - Self::new( - [FactorSourceID::sample()], - 1, - [FactorSourceID::sample_other()], - ) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - [FactorSourceID::sample_other()], - 0, - [FactorSourceID::sample()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = ConfirmationRoleWithFactorSourceIDs; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/matrix_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/matrix_of_factor_source_ids.rs deleted file mode 100644 index 900dd75ba..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/matrix_of_factor_source_ids.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::prelude::*; - -impl From for MatrixOfFactorSourceIDs { - fn from(value: MatrixOfFactorSources) -> Self { - Self::new( - value.primary_role.into(), - value.recovery_role.into(), - value.confirmation_role.into(), - ) - .unwrap() - } -} - -impl HasSampleValues for MatrixOfFactorSourceIDs { - fn sample() -> Self { - Self::new( - PrimaryRoleWithFactorSourceIDs::sample(), - RecoveryRoleWithFactorSourceIDs::sample(), - ConfirmationRoleWithFactorSourceIDs::sample(), - ) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - PrimaryRoleWithFactorSourceIDs::sample_other(), - RecoveryRoleWithFactorSourceIDs::sample_other(), - ConfirmationRoleWithFactorSourceIDs::sample_other(), - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = MatrixOfFactorSourceIDs; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/mod.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/mod.rs deleted file mode 100644 index a7b0f2d88..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -mod confirmation_role_with_factor_source_ids; -mod matrix_of_factor_source_ids; -mod primary_role_with_factor_source_ids; -mod recovery_role_with_factor_source_ids; -mod security_structure_id; -mod security_structure_of_factor_source_ids; -mod security_structures_of_factor_source_ids; - -pub use confirmation_role_with_factor_source_ids::*; -pub use matrix_of_factor_source_ids::*; -pub use primary_role_with_factor_source_ids::*; -pub use recovery_role_with_factor_source_ids::*; -pub use security_structure_id::*; -pub use security_structure_of_factor_source_ids::*; -pub use security_structures_of_factor_source_ids::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/primary_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/primary_role_with_factor_source_ids.rs deleted file mode 100644 index a96ce8a5d..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/primary_role_with_factor_source_ids.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::prelude::*; - -impl From for PrimaryRoleWithFactorSourceIDs { - fn from(value: PrimaryRoleWithFactorSources) -> Self { - Self::new( - value.threshold_factors.iter().map(|x| x.factor_source_id()), - value.threshold, - value.override_factors.iter().map(|x| x.factor_source_id()), - ) - .expect("PrimaryRoleWithFactorSources has already been validated.") - } -} - -impl HasSampleValues for PrimaryRoleWithFactorSourceIDs { - fn sample() -> Self { - Self::threshold_factors_only( - [FactorSourceID::sample(), FactorSourceID::sample_other()], - 2, - ) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - [FactorSourceID::sample()], - 1, - [FactorSourceID::sample_other()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = PrimaryRoleWithFactorSourceIDs; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/recovery_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/recovery_role_with_factor_source_ids.rs deleted file mode 100644 index 2c777f9ee..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/recovery_role_with_factor_source_ids.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::prelude::*; - -impl From for RecoveryRoleWithFactorSourceIDs { - fn from(value: RecoveryRoleWithFactorSources) -> Self { - Self::new( - value.threshold_factors.iter().map(|x| x.factor_source_id()), - value.threshold, - value.override_factors.iter().map(|x| x.factor_source_id()), - ) - .expect("RecoveryRoleWithFactorSources has already been validated.") - } -} - -impl HasSampleValues for RecoveryRoleWithFactorSourceIDs { - fn sample() -> Self { - Self::threshold_factors_only([FactorSourceID::sample_other()], 1) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - [FactorSourceID::sample()], - 1, - [FactorSourceID::sample_other()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = RecoveryRoleWithFactorSourceIDs; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_of_factor_source_ids.rs deleted file mode 100644 index 9c4904a8a..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_of_factor_source_ids.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::prelude::*; - -decl_security_structure_of!( - /// A security structure at FactorSourceID level, this is - /// what is serialized and store into Profile, we convert - /// into this structure from `SecurityStructureOfFactorSources`. - FactorSourceID, -); - -impl Identifiable for SecurityStructureOfFactorSourceIDs { - type ID = ::ID; - - fn id(&self) -> Self::ID { - self.metadata.id() - } -} - -impl From - for SecurityStructureOfFactorSourceIDs -{ - fn from(value: SecurityStructureOfFactorSources) -> Self { - Self::new( - value.metadata, - value.number_of_epochs_until_auto_confirmation, - value.matrix_of_factors.into(), - ) - } -} - -impl HasSampleValues for SecurityStructureOfFactorSourceIDs { - fn sample() -> Self { - SecurityStructureOfFactorSources::sample().into() - } - fn sample_other() -> Self { - SecurityStructureOfFactorSources::sample_other().into() - } -} - -#[cfg(test)] -mod test_schematic_of_security_shield { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = SecurityStructureOfFactorSourceIDs; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structures_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structures_of_factor_source_ids.rs deleted file mode 100644 index 0796f9bb0..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structures_of_factor_source_ids.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::prelude::*; - -decl_identified_vec_of!( - /// A collection of [`SecurityStructureOfFactorSourceIDs`] - SecurityStructuresOfFactorSourceIDs, - SecurityStructureOfFactorSourceIDs -); - -impl HasSampleValues for SecurityStructuresOfFactorSourceIDs { - fn sample() -> Self { - Self::from_iter([ - SecurityStructureOfFactorSourceIDs::sample(), - SecurityStructureOfFactorSourceIDs::sample_other(), - ]) - } - fn sample_other() -> Self { - Self::from_iter([SecurityStructureOfFactorSourceIDs::sample_other()]) - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/matrix_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_level/matrix_of_factor_sources.rs deleted file mode 100644 index 442a959fe..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/matrix_of_factor_sources.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::prelude::*; - -impl HasSampleValues for MatrixOfFactorSources { - fn sample() -> Self { - Self::new( - PrimaryRoleWithFactorSources::sample(), - RecoveryRoleWithFactorSources::sample(), - ConfirmationRoleWithFactorSources::sample(), - ) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - PrimaryRoleWithFactorSources::sample_other(), - RecoveryRoleWithFactorSources::sample_other(), - ConfirmationRoleWithFactorSources::sample_other(), - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = MatrixOfFactorSources; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/primary_role_with_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_level/primary_role_with_factor_sources.rs deleted file mode 100644 index a4e542727..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/primary_role_with_factor_sources.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::prelude::*; - -impl HasSampleValues for PrimaryRoleWithFactorSources { - fn sample() -> Self { - Self::new( - [ - FactorSource::sample_device_babylon(), - FactorSource::sample_arculus(), - FactorSource::sample_off_device(), - ], - 2, - [FactorSource::sample_ledger()], - ) - .unwrap() - } - fn sample_other() -> Self { - Self::new( - [ - FactorSource::sample_device_babylon_other(), - FactorSource::sample_arculus_other(), - FactorSource::sample_off_device_other(), - ], - 2, - [FactorSource::sample_ledger_other()], - ) - .unwrap() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = PrimaryRoleWithFactorSources; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structure_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structure_of_factor_sources.rs deleted file mode 100644 index 1a221f115..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structure_of_factor_sources.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::prelude::*; - -decl_security_structure_of!( - /// Security structure at `FactorSource` level. - /// This is what user view, creates and manages. - /// - /// Before it gets saved into Profile gets converted into - /// `SecurityStructureOfFactorSourceIDs` - FactorSource, -); - -pub const MINUTES_PER_DAY: u64 = 24 * 60; -pub const MINUTES_PER_EPOCH: u64 = 5; -pub const EPOCHS_PER_DAY: u64 = MINUTES_PER_DAY / MINUTES_PER_EPOCH; - -pub fn days_to_epochs(days: u16) -> u64 { - let days = days as u64; - days * EPOCHS_PER_DAY -} - -impl SecurityStructureOfFactorSources { - pub fn new_with_days( - metadata: SecurityStructureMetadata, - number_of_days_until_auto_confirmation: u16, - matrix_of_factors: MatrixOfFactorSources, - ) -> Self { - Self::new( - metadata, - days_to_epochs(number_of_days_until_auto_confirmation), - matrix_of_factors, - ) - } -} - -impl Identifiable for SecurityStructureOfFactorSources { - type ID = ::ID; - - fn id(&self) -> Self::ID { - self.metadata.id() - } -} - -fn factors_from( - ids: &[FactorSourceID], - from: &FactorSources, -) -> Result { - ids.iter() - .map(|id| { - from.get_id(*id) - .ok_or(CommonError::ProfileDoesNotContainFactorSourceWithID { - bad_value: *id, - }) - .cloned() - }) - .collect::>() -} - -impl TryFrom<(&PrimaryRoleWithFactorSourceIDs, &FactorSources)> - for PrimaryRoleWithFactorSources -{ - type Error = CommonError; - fn try_from( - value: (&PrimaryRoleWithFactorSourceIDs, &FactorSources), - ) -> Result { - let (id_level, factor_sources) = value; - - let threshold_factors = - factors_from(&id_level.threshold_factors, factor_sources)?; - - let override_factors = - factors_from(&id_level.override_factors, factor_sources)?; - Self::new(threshold_factors, id_level.threshold, override_factors) - } -} - -impl TryFrom<(&RecoveryRoleWithFactorSourceIDs, &FactorSources)> - for RecoveryRoleWithFactorSources -{ - type Error = CommonError; - fn try_from( - value: (&RecoveryRoleWithFactorSourceIDs, &FactorSources), - ) -> Result { - let (id_level, factor_sources) = value; - - let threshold_factors = - factors_from(&id_level.threshold_factors, factor_sources)?; - - let override_factors = - factors_from(&id_level.override_factors, factor_sources)?; - Self::new(threshold_factors, id_level.threshold, override_factors) - } -} - -impl TryFrom<(&ConfirmationRoleWithFactorSourceIDs, &FactorSources)> - for ConfirmationRoleWithFactorSources -{ - type Error = CommonError; - fn try_from( - value: (&ConfirmationRoleWithFactorSourceIDs, &FactorSources), - ) -> Result { - let (id_level, factor_sources) = value; - - let threshold_factors = - factors_from(&id_level.threshold_factors, factor_sources)?; - - let override_factors = - factors_from(&id_level.override_factors, factor_sources)?; - Self::new(threshold_factors, id_level.threshold, override_factors) - } -} - -impl TryFrom<(&MatrixOfFactorSourceIDs, &FactorSources)> - for MatrixOfFactorSources -{ - type Error = CommonError; - fn try_from( - value: (&MatrixOfFactorSourceIDs, &FactorSources), - ) -> Result { - let (id_level, factor_sources) = value; - let primary_role = PrimaryRoleWithFactorSources::try_from(( - &id_level.primary_role, - factor_sources, - ))?; - - let recovery_role = RecoveryRoleWithFactorSources::try_from(( - &id_level.recovery_role, - factor_sources, - ))?; - - let confirmation_role = ConfirmationRoleWithFactorSources::try_from(( - &id_level.confirmation_role, - factor_sources, - ))?; - - Self::new(primary_role, recovery_role, confirmation_role) - } -} - -impl TryFrom<(&SecurityStructureOfFactorSourceIDs, &FactorSources)> - for SecurityStructureOfFactorSources -{ - type Error = CommonError; - fn try_from( - value: (&SecurityStructureOfFactorSourceIDs, &FactorSources), - ) -> Result { - let (id_level, factor_sources) = value; - let matrix = MatrixOfFactorSources::try_from(( - &id_level.matrix_of_factors, - factor_sources, - ))?; - Ok(Self::new( - id_level.metadata.clone(), - id_level.number_of_epochs_until_auto_confirmation, - matrix, - )) - } -} - -impl HasSampleValues for SecurityStructureOfFactorSources { - fn sample() -> Self { - Self::new_with_days( - SecurityStructureMetadata::sample(), - 14, - MatrixOfFactorSources::sample(), - ) - } - fn sample_other() -> Self { - Self::new_with_days( - SecurityStructureMetadata::sample_other(), - 28, - MatrixOfFactorSources::sample_other(), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = SecurityStructureOfFactorSources; - - #[test] - fn equality() { - assert_eq!(SUT::sample(), SUT::sample()); - assert_eq!(SUT::sample_other(), SUT::sample_other()); - } - - #[test] - fn inequality() { - assert_ne!(SUT::sample(), SUT::sample_other()); - } - - #[test] - fn test_epochs_per_day() { - assert_eq!(EPOCHS_PER_DAY, 288); - } - - #[test] - fn test_days_to_epochs() { - assert_eq!(days_to_epochs(0), 0); - assert_eq!(days_to_epochs(10), 2880); - } - - #[test] - fn test_into_id_level_and_back() { - let factor_sources = FactorSources::sample_values_all(); - let sut = SUT::sample(); - let id_level = SecurityStructureOfFactorSourceIDs::from(sut.clone()); - let detailed = SUT::try_from((&id_level, &factor_sources)).unwrap(); - assert_eq!(detailed, sut); - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structures_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structures_of_factor_sources.rs deleted file mode 100644 index d64ef90fc..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/security_structures_of_factor_sources.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::prelude::*; - -decl_identified_vec_of!( - /// A collection of [`SecurityStructureOfFactorSources`] - SecurityStructuresOfFactorSources, - SecurityStructureOfFactorSources -); - -impl HasSampleValues for SecurityStructuresOfFactorSources { - fn sample() -> Self { - Self::from_iter([ - SecurityStructureOfFactorSources::sample(), - SecurityStructureOfFactorSources::sample_other(), - ]) - } - fn sample_other() -> Self { - Self::from_iter([SecurityStructureOfFactorSources::sample_other()]) - } -} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/abstract_matrix_builder_or_built.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/abstract_matrix_builder_or_built.rs new file mode 100644 index 000000000..534dbd244 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/abstract_matrix_builder_or_built.rs @@ -0,0 +1,112 @@ +#![allow(non_camel_case_types)] + +use crate::prelude::*; + +/// One of two possible `MODE_OF_MATRIX` values, used for the **builder of a matrix**. +pub const IS_MATRIX_BUILDER: u8 = 0; +/// One of two possible `MODE_OF_MATRIX` values, used for the **built matrix**. +pub const IS_BUILT_MATRIX: u8 = 1; + +/// One of two possible `MODE_OF_ROLE` values, used for the **builder of roles**. +pub const IS_ROLE_BUILDER: u8 = 0; + +/// One of two possible `MODE_OF_ROLE` values, used for the **built roles**. +pub const IS_BUILT_ROLE: u8 = 0; + +/// Either a matrix or a **builder of a matrix** with a Primary, Recovery and Confirmation +/// role or **builder of roles**. +/// This type is shared by: +/// * MatrixBuilder (FactorSourceID) +/// +/// # Built +/// * MatrixOfFactorSources +/// * MatrixOfFactorSourceIds +/// * MatrixOfFactorInstances +/// +/// For "built types" the `built` field is `PhantomData<()>`, for the `MatrixBuilder` +/// it is `PhantomData`. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractMatrixBuilderOrBuilt< + const MODE_OF_MATRIX: u8, + const MODE_OF_ROLE: u8, + FACTOR, +> { + pub(crate) primary_role: + AbstractRoleBuilderOrBuilt<{ ROLE_PRIMARY }, MODE_OF_ROLE, FACTOR>, + pub(crate) recovery_role: + AbstractRoleBuilderOrBuilt<{ ROLE_RECOVERY }, MODE_OF_ROLE, FACTOR>, + pub(crate) confirmation_role: + AbstractRoleBuilderOrBuilt<{ ROLE_CONFIRMATION }, MODE_OF_ROLE, FACTOR>, + + pub number_of_days_until_auto_confirm: u16, +} + +impl + AbstractMatrixBuilderOrBuilt +{ + pub const DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM: u16 = 14; + + /// # Safety + /// Rust memory safe, but marked "unsafe" since it might allow for instantiation + /// of unsafe - as in application **unsecure** - MatrixOfFactors, which might + /// lead to increase risk for end user to loose funds. + pub unsafe fn unbuilt_with_roles_and_days( + primary_role: AbstractRoleBuilderOrBuilt< + { ROLE_PRIMARY }, + MODE_OF_ROLE, + FACTOR, + >, + recovery_role: AbstractRoleBuilderOrBuilt< + { ROLE_RECOVERY }, + MODE_OF_ROLE, + FACTOR, + >, + confirmation_role: AbstractRoleBuilderOrBuilt< + { ROLE_CONFIRMATION }, + MODE_OF_ROLE, + FACTOR, + >, + number_of_days_until_auto_confirm: u16, + ) -> Self { + Self { + primary_role, + recovery_role, + confirmation_role, + number_of_days_until_auto_confirm, + } + } +} + +pub type AbstractMatrixBuilt = + AbstractMatrixBuilderOrBuilt; + +impl AbstractMatrixBuilt { + pub fn primary( + &self, + ) -> &AbstractBuiltRoleWithFactor<{ ROLE_PRIMARY }, FACTOR> { + &self.primary_role + } + + pub fn recovery( + &self, + ) -> &AbstractBuiltRoleWithFactor<{ ROLE_RECOVERY }, FACTOR> { + &self.recovery_role + } + + pub fn confirmation( + &self, + ) -> &AbstractBuiltRoleWithFactor<{ ROLE_CONFIRMATION }, FACTOR> { + &self.confirmation_role + } +} + +impl AbstractMatrixBuilt { + pub fn all_factors(&self) -> HashSet<&FACTOR> { + let mut factors = HashSet::new(); + factors.extend(self.primary_role.all_factors()); + factors.extend(self.recovery_role.all_factors()); + factors.extend(self.confirmation_role.all_factors()); + factors + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/builder/error.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/error.rs new file mode 100644 index 000000000..e7a181792 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/error.rs @@ -0,0 +1,64 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationViolation { + #[error("Basic violation: {0}")] + Basic(#[from] MatrixRolesInCombinationBasicViolation), + + #[error("Forever invalid: {0}")] + ForeverInvalid(#[from] MatrixRolesInCombinationForeverInvalid), + + #[error("Not yet valid: {0}")] + NotYetValid(#[from] MatrixRolesInCombinationNotYetValid), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationBasicViolation { + #[error("The factor source was not found in any role")] + FactorSourceNotFoundInAnyRole, + + #[error("The number of days until auto confirm must be greater than zero")] + NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationForeverInvalid { + #[error("Recovery and confirmation factors overlap. No factor may be used in both the recovery and confirmation roles")] + RecoveryAndConfirmationFactorsOverlap, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixRolesInCombinationNotYetValid { + #[error("The single factor used in the primary role must not be used in any other role")] + SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum MatrixBuilderValidation { + #[error("Role {role:?} in isolation violation: {violation}")] + RoleInIsolation { + role: RoleKind, + violation: RoleBuilderValidation, + }, + #[error("Roles in combination violation: {0}")] + CombinationViolation(#[from] MatrixRolesInCombinationViolation), +} + +pub(crate) trait IntoMatrixErr { + fn into_matrix_err( + self, + role: RoleKind, + ) -> Result; +} + +impl IntoMatrixErr for Result { + fn into_matrix_err( + self, + role: RoleKind, + ) -> Result { + self.map_err(|violation| MatrixBuilderValidation::RoleInIsolation { + role, + violation, + }) + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder.rs new file mode 100644 index 000000000..1423eaf25 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder.rs @@ -0,0 +1,406 @@ +#![allow(clippy::new_without_default)] + +use crate::prelude::*; + +pub type MatrixBuilderMutateResult = Result<(), MatrixBuilderValidation>; + +pub type MatrixBuilderBuildResult = + Result; + +/// A builder of MatrixOfFactorSourceIds, consists of role builders: +/// * PrimaryRoleBuilder +/// * RecoveryRoleBuilder +/// * ConfirmationRoleBuilder +/// +/// And `number_of_days_until_auto_confirm`. +pub type MatrixBuilder = AbstractMatrixBuilderOrBuilt< + IS_MATRIX_BUILDER, + IS_ROLE_BUILDER, + FactorSourceID, +>; + +// ================== +// ===== PUBLIC ===== +// ================== +impl MatrixBuilder { + pub fn new() -> Self { + Self { + primary_role: PrimaryRoleBuilder::new(), + recovery_role: RecoveryRoleBuilder::new(), + confirmation_role: ConfirmationRoleBuilder::new(), + number_of_days_until_auto_confirm: + Self::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + } + } + + /// Validates each role in isolation and all roles in combination. + /// + /// If valid it returns a "built" `MatrixOfFactorSourceIds`. + pub fn build(&self) -> MatrixBuilderBuildResult { + self.validate_combination()?; + + let primary = self + .primary_role + .build() + .into_matrix_err(RoleKind::Primary)?; + let recovery = self + .recovery_role + .build() + .into_matrix_err(RoleKind::Recovery)?; + let confirmation = self + .confirmation_role + .build() + .into_matrix_err(RoleKind::Confirmation)?; + + let built = unsafe { + // Looks a bit odd, but yeah here is in fact the only place we + // do build! The ctor is named like that so that it is clear that + // when used elsewhere, it is not guaranteed to have been properly + // built. + MatrixOfFactorSourceIds::unbuilt_with_roles_and_days( + primary, + recovery, + confirmation, + self.number_of_days_until_auto_confirm, + ) + }; + Ok(built) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.primary_role + .validation_for_addition_of_factor_source_of_kind_to_threshold( + factor_source_kind, + ) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_primary_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.primary_role + .validation_for_addition_of_factor_source_of_kind_to_override( + factor_source_kind, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.primary_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.primary_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_recovery_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.recovery_role + .validation_for_addition_of_factor_source_of_kind_to_override( + factor_source_kind, + ) + } + + pub fn validation_for_addition_of_factor_source_to_recovery_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.recovery_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + pub fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.confirmation_role + .validation_for_addition_of_factor_source_of_kind_to_override( + factor_source_kind, + ) + } + + pub fn validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &self, + factor_sources: &IndexSet, + ) -> IndexSet { + self.confirmation_role + .validation_for_addition_of_factor_source_for_each( + FactorListKind::Override, + factor_sources, + ) + } + + fn validate_each_role_in_isolation(&self) -> MatrixBuilderMutateResult { + self.primary_role + .validate() + .into_matrix_err(RoleKind::Primary)?; + self.recovery_role + .validate() + .into_matrix_err(RoleKind::Recovery)?; + self.confirmation_role + .validate() + .into_matrix_err(RoleKind::Confirmation)?; + Ok(()) + } + + pub fn validate(&self) -> MatrixBuilderMutateResult { + self.validate_each_role_in_isolation()?; + self.validate_combination()?; + Ok(()) + } + + /// Adds the factor source to the primary role threshold list. + /// + /// Also sets the threshold to 1 this is the first factor set and if + /// the threshold was 0. + pub fn add_factor_source_to_primary_threshold( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.primary_role + .add_factor_source_to_threshold(factor_source_id) + .into_matrix_err(RoleKind::Primary) + } + + pub fn reset_recovery_and_confirmation_role_state(&mut self) { + self.recovery_role.reset(); + self.confirmation_role.reset(); + } + + /// Adds the factor source to the primary role override list. + pub fn add_factor_source_to_primary_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.primary_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Primary) + } + + pub fn add_factor_source_to_recovery_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.recovery_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Recovery) + } + + pub fn add_factor_source_to_confirmation_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> MatrixBuilderMutateResult { + self.confirmation_role + .add_factor_source_to_override(factor_source_id) + .into_matrix_err(RoleKind::Confirmation) + } + + pub fn get_confirmation_factors(&self) -> &Vec { + self.confirmation_role.get_override_factors() + } + + pub fn get_recovery_factors(&self) -> &Vec { + self.recovery_role.get_override_factors() + } + + pub fn get_primary_threshold_factors(&self) -> &Vec { + self.primary_role.get_threshold_factors() + } + + pub fn get_primary_override_factors(&self) -> &Vec { + self.primary_role.get_override_factors() + } + + /// Sets the threshold on the primary role builder. + pub fn set_threshold( + &mut self, + threshold: u8, + ) -> MatrixBuilderMutateResult { + self.primary_role + .set_threshold(threshold) + .into_matrix_err(RoleKind::Primary) + } + + pub fn get_threshold(&self) -> u8 { + self.primary_role.get_threshold() + } + + pub fn set_number_of_days_until_auto_confirm( + &mut self, + number_of_days: u16, + ) -> MatrixBuilderMutateResult { + self.number_of_days_until_auto_confirm = number_of_days; + + self.validate_number_of_days_until_auto_confirm() + } + + pub fn get_number_of_days_until_auto_confirm(&self) -> u16 { + self.number_of_days_until_auto_confirm + } + + fn remove_factor_from_role( + role: &mut RoleBuilder<{ ROLE }>, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + if role.remove_factor_source(factor_source_id).is_ok() { + Ok(()) + } else { + MatrixBuilderMutateResult::Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::FactorSourceNotFoundInAnyRole, + ), + )) + } + } + + pub fn remove_factor_from_primary( + &mut self, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + Self::remove_factor_from_role(&mut self.primary_role, factor_source_id) + } + + pub fn remove_factor_from_recovery( + &mut self, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + Self::remove_factor_from_role(&mut self.recovery_role, factor_source_id) + } + + pub fn remove_factor_from_confirmation( + &mut self, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + Self::remove_factor_from_role( + &mut self.confirmation_role, + factor_source_id, + ) + } + + /// Removes `factor_source_id` from all three roles, if not found in any an error + /// is thrown. + /// + /// # Throws + /// If none of the three role builders contains the factor source id, `Err(BasicViolation::FactorSourceNotFound)` is thrown + pub fn remove_factor_from_all_roles( + &mut self, + factor_source_id: &FactorSourceID, + ) -> MatrixBuilderMutateResult { + let fsid = factor_source_id; + let r0 = self.remove_factor_from_primary(fsid); + let r1 = self.remove_factor_from_recovery(fsid); + let r2 = self.remove_factor_from_confirmation(fsid); + r0.or(r1).or(r2) + } +} + +// ================== +// ==== PRIVATE ===== +// ================== +impl MatrixBuilder { + fn validate_if_primary_has_single_it_must_not_be_used_by_any_other_role( + &self, + ) -> MatrixBuilderMutateResult { + let primary_has_single_factor = + self.primary_role.all_factors().len() == 1; + if primary_has_single_factor { + let primary_factors = self.primary_role.all_factors(); + let primary_factor = primary_factors.first().unwrap(); + let recovery_set = HashSet::<_>::from_iter( + self.recovery_role.get_override_factors(), + ); + let confirmation_set = HashSet::<_>::from_iter( + self.confirmation_role.get_override_factors(), + ); + if recovery_set.contains(primary_factor) + || confirmation_set.contains(primary_factor) + { + return Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole), + )); + } + } + Ok(()) + } + + fn validate_no_factor_may_be_used_in_both_recovery_and_confirmation( + &self, + ) -> MatrixBuilderMutateResult { + let recovery_set = + HashSet::<_>::from_iter(self.recovery_role.get_override_factors()); + let confirmation_set = HashSet::<_>::from_iter( + self.confirmation_role.get_override_factors(), + ); + let intersection = recovery_set + .intersection(&confirmation_set) + .collect::>(); + if intersection.is_empty() { + Ok(()) + } else { + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap, + ), + )) + } + } + + fn validate_number_of_days_until_auto_confirm( + &self, + ) -> MatrixBuilderMutateResult { + if self.number_of_days_until_auto_confirm == 0 { + return Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, + ), + )); + } + Ok(()) + } + + /// Security Shield Rules + /// In addition to the factor/role rules above, the wallet must enforce certain rules for combinations of + /// factors across the three roles. The construction method described in the next section will automatically + /// always follow these rules. A user may however choose to manually add/remove factors from their Shield + /// configuration and so the wallet must evaluate these rules and inform the user when the combination they + /// have chosen cannot be used. The wallet should never allow a user to complete a Shield configuration that + /// violates these rules. + /// + /// 1. If only one factor is used for `Primary`, that factor may not be used for either `Recovery` or `Confirmation` + /// 2. No factor may be used (override) in both `Recovery` and `Confirmation` + /// 3. No factor may be used in both the `Primary` threshold and `Primary` override + /// 4. Number of days until auto confirm is greater than zero + fn validate_combination(&self) -> MatrixBuilderMutateResult { + self.validate_if_primary_has_single_it_must_not_be_used_by_any_other_role()?; + self.validate_no_factor_may_be_used_in_both_recovery_and_confirmation( + )?; + + // N.B. the third 3: + // "3. No factor may be used in both the `Primary` threshold and `Primary` override" + // is already enforced by the RoleBuilder + + self.validate_number_of_days_until_auto_confirm()?; + Ok(()) + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder_unit_tests.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder_unit_tests.rs new file mode 100644 index 000000000..f7a6f6089 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_builder_unit_tests.rs @@ -0,0 +1,1994 @@ +#![cfg(test)] +use crate::prelude::*; + +#[allow(clippy::upper_case_acronyms)] +type SUT = MatrixBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[test] +fn empty_primary_is_err() { + let sut = make(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err( + MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Primary, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + } + ) + ) +} + +#[test] +fn empty_recovery_is_err() { + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err( + MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Recovery, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + } + ) + ) +} + +#[test] +fn empty_confirmation_is_err() { + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_ledger()) + .unwrap(); + + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + let res = sut.build(); + assert_eq!( + res, + MatrixBuilderBuildResult::Err( + MatrixBuilderValidation::RoleInIsolation { + role: RoleKind::Confirmation, + violation: RoleBuilderValidation::NotYetValid( + NotYetValidReason::RoleMustHaveAtLeastOneFactor + ) + } + ) + ) +} + +#[test] +fn set_number_of_days_cannot_be_zero() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + sut.number_of_days_until_auto_confirm = 0; // bypass validation + + // Build + let validation = MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic(MatrixRolesInCombinationBasicViolation::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero) + ); + assert_eq!(sut.validate(), Err(validation)); + let res = sut.build(); + assert_eq!(res, Err(validation)); +} + +#[test] +fn set_number_of_days_42() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + sut.set_number_of_days_until_auto_confirm(42).unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles_and_days( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + 42, + ) + ); +} + +#[test] +fn auto_confirm_default() { + assert_eq!(SUT::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, 14); +} + +#[test] +fn set_number_of_days_if_not_set_uses_default() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_device()) + .unwrap(); + + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_ledger()) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles_and_days( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + SUT::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + ) + ); +} + +#[test] +fn sample_factor_cannot_be_both_in_threshold_and_override() { + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + let res = sut.add_factor_source_to_primary_override(fs); + assert!(res.is_err()); +} + +#[test] +fn single_factor_in_primary_threshold_cannot_be_in_recovery() { + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus_other(), + ) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // ACT + sut.add_factor_source_to_recovery_override(fs).unwrap(); + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); + + sut.add_factor_source_to_primary_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built.primary(), + &RoleWithFactorSourceIds::primary_with_factors( + 1, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ], + [] + ) + ); + pretty_assertions::assert_eq!( + built.recovery(), + &RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger() + ]), + ); + + pretty_assertions::assert_eq!( + built.confirmation(), + &RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_arculus_other() + ]) + ) +} + +#[test] +fn single_factor_in_primary_override_cannot_be_in_recovery() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn single_factor_in_primary_threshold_cannot_be_in_confirmation() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + _ = sut.set_threshold(1); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn single_factor_in_primary_override_cannot_be_in_confirmation() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_recovery_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +#[test] +fn add_factor_to_recovery_then_same_to_confirmation_is_err() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap + ) + )) + ); +} + +#[test] +fn add_factor_to_confirmation_then_same_to_override_when_validated_is_err() { + // ARRANGE + let mut sut = make(); + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // ACT + sut.add_factor_source_to_recovery_override(fs).unwrap(); + sut.add_factor_source_to_confirmation_override(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::ForeverInvalid( + MatrixRolesInCombinationForeverInvalid::RecoveryAndConfirmationFactorsOverlap + ) + )) + ); +} + +#[test] +fn add_factor_to_confirmation_then_same_to_primary_threshold_is_not_yet_valid() +{ + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + _ = sut.set_threshold(1); + + // ACT + let fs = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_recovery_override(fs).unwrap(); + sut.add_factor_source_to_primary_threshold(fs).unwrap(); + + // ASSERT + let res = sut.validate(); + assert_eq!(res, Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::NotYetValid(MatrixRolesInCombinationNotYetValid::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + ))); +} + +mod remove { + use super::*; + + #[test] + fn not_found() { + let mut sut = make(); + let res = + sut.remove_factor_from_all_roles(&FactorSourceID::sample_device()); + assert_eq!( + res, + Err(MatrixBuilderValidation::CombinationViolation( + MatrixRolesInCombinationViolation::Basic( + MatrixRolesInCombinationBasicViolation::FactorSourceNotFoundInAnyRole + ) + )) + ); + } + + #[test] + fn remove_from_primary_threshold_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + assert_eq!( + sut.primary_role.get_threshold_factors(), + &[FactorSourceID::sample_device()] + ); + let res = + sut.remove_factor_from_all_roles(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_primary_override_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = + sut.remove_factor_from_primary(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_recovery_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = + sut.remove_factor_from_recovery(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } + + #[test] + fn remove_from_confirmation_is_ok() { + let mut sut = make(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut + .remove_factor_from_confirmation(&FactorSourceID::sample_device()); + assert_eq!(res, Ok(())); + } +} + +mod validation_for_addition_of_factor_source_for_each { + use super::*; + + mod primary { + + use super::*; + + #[test] + fn empty() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn device_threshold_3x_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_2x_threshold_override_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_threshold_override_2x_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn device_2x_override_threshold_first_ok_second_not() { + let mut sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_primary_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ) + ] + ); + + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ]), + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + } + + mod recovery { + use super::*; + + fn role() -> RoleKind { + RoleKind::Recovery + } + + #[test] + fn empty() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn supported() { + let sut = make(); + let fsids = vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other(), + FactorSourceID::sample_off_device(), + FactorSourceID::sample_off_device_other(), + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ]; + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::from_iter(fsids.clone()), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + fsids + .into_iter() + .map(|fsid| FactorSourceInRoleBuilderValidationStatus::ok( + role(), + fsid + )) + .collect::>() + ); + } + + #[test] + fn password_and_security_questions_not_supported() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ]), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + role(), + fsid, + if fsid.get_factor_source_kind() == FactorSourceKind::SecurityQuestions { + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + } else { + ForeverInvalidReason::RecoveryRolePasswordNotSupported + } + ) + ) + .collect::>() + ); + } + } + + mod confirmation { + use super::*; + + fn role() -> RoleKind { + RoleKind::Confirmation + } + + #[test] + fn empty() { + let sut = make(); + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::new(), + ); + assert_eq!(xs, IndexSet::new()); + } + + #[test] + fn supported() { + let sut = make(); + let fsids = vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_off_device(), + FactorSourceID::sample_off_device_other(), + ]; + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::from_iter(fsids.clone()), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + fsids + .into_iter() + .map(|fsid| FactorSourceInRoleBuilderValidationStatus::ok( + role(), + fsid + )) + .collect::>() + ); + } + + #[test] + fn password_and_security_questions_not_supported() { + let sut = make(); + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &IndexSet::from_iter([ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ]), + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + role(), + fsid, + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ) + .collect::>() + ); + } + } +} + +mod validation_of_addition_of_kind { + use super::*; + + mod recovery { + use super::*; + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_recovery_override_empty( + ) { + let sut = make(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_recovery_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, false); + test(FactorSourceKind::Password, false); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, true); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_recovery_override_single_recovery( + ) { + let mut sut = make(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_recovery_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, false); + test(FactorSourceKind::Password, false); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, true); + } + } + + mod confirmation { + use super::*; + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override_empty( + ) { + let sut = make(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_confirmation_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, true); + test(FactorSourceKind::Password, true); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, false); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_confirmation_override_single_recovery( + ) { + let mut sut = make(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + let test = |kind: FactorSourceKind, should_be_ok: bool| { + let is_ok = sut + .validation_for_addition_of_factor_source_of_kind_to_confirmation_override(kind) + .is_ok(); + assert_eq!(is_ok, should_be_ok); + }; + test(FactorSourceKind::Device, true); + test(FactorSourceKind::LedgerHQHardwareWallet, true); + test(FactorSourceKind::ArculusCard, true); + test(FactorSourceKind::SecurityQuestions, true); + test(FactorSourceKind::Password, true); + test(FactorSourceKind::OffDeviceMnemonic, true); + test(FactorSourceKind::TrustedContact, false); + } + } + + mod primary { + use super::*; + + #[test] + fn ledger_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn ledger_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::LedgerHQHardwareWallet, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn arculus_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::ArculusCard, + ); + assert!(res.is_ok()); + } + + #[test] + fn security_questions_not_supported_threshold() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::SecurityQuestions, + ); + assert!(res.is_err()); + } + + #[test] + fn security_questions_not_supported_override() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::SecurityQuestions, + ); + assert!(res.is_err()); + } + + #[test] + fn trusted_contact_not_supported_threshold() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::TrustedContact, + ); + assert!(res.is_err()); + } + + #[test] + fn trusted_contact_not_supported_override() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::TrustedContact, + ); + assert!(res.is_err()); + } + + #[test] + fn passphrase_threshold_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_off_device(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_threshold_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_off_device(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_override_override() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_off_device(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn passphrase_override_threshold() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_off_device(), + ) + .unwrap(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::OffDeviceMnemonic, + ); + assert!(res.is_ok()); + } + + #[test] + fn thresehold_password_alone_is_err() { + // ARRANGE + let sut = make(); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Password, + ); + assert!(res.is_err()); + } + + #[test] + fn thresehold_password_not_alone() { + // ARRANGE + let mut sut = make(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_arculus(), + ) + .unwrap(); + _ = sut.set_threshold(2); + + // ASSERT + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Password, + ); + assert!(res.is_ok()); + } + + #[test] + fn device_is_err_for_second_3x_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_2x_threshold_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_threshold_override_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_threshold_override_2x() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_3x_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_2x_override_threshold() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_override_threshold_2x() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + + #[test] + fn device_is_err_for_second_override_threshold_override() { + let mut sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_ok()); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_primary_override( + FactorSourceKind::Device, + ); + assert!(res.is_err()); + } + } +} + +mod shield_configs { + use super::*; + + mod mvp { + + use super::*; + + #[test] + fn config_1_1() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + let build0 = sut.build(); // build err + assert!(build0.is_err()); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + let built0 = sut.build().unwrap(); + + // Recovery - re + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + let built2 = sut.build().unwrap(); + assert_ne!(built0, built); // we changed recovery since! + assert_eq!(built2, built); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + assert_eq!(built, MatrixOfFactorSourceIds::sample_config_1_1()); + } + + #[test] + fn config_1_2() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + let res = sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_password(), + ); + + assert_eq!( + res, + Err(MatrixBuilderValidation::RoleInIsolation { role: RoleKind::Primary, violation: RoleBuilderValidation::NotYetValid(NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne)} + )); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_password() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + } + + #[test] + fn config_1_3() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + let res = sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_password(), + ); + + assert_eq!( + res, + Err(MatrixBuilderValidation::RoleInIsolation { role: RoleKind::Primary, violation: RoleBuilderValidation::NotYetValid(NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne)} + )); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_password() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_1_3() + ) + } + + #[test] + fn config_1_4() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_1_4() + ) + } + + #[test] + fn config_1_5() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_ledger(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device() + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_1_5() + ) + } + + #[test] + fn config_2_1() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_2_1() + ) + } + + #[test] + fn config_2_2() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_2_2() + ) + } + + #[test] + fn config_2_3() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_ledger(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_2_3() + ) + } + + #[test] + fn config_2_4() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.set_threshold(1).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 1, + [FactorSourceID::sample_device(),], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_ledger_other() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_2_4() + ) + } + + #[test] + fn config_3_0() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_password() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_3_0() + ) + } + + #[test] + fn config_4_0() { + let mut sut = make(); + + // Primary + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + sut.set_threshold(2).unwrap(); + + // Recovery + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_device(), + ) + .unwrap(); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + + // Confirmation + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password(), + ) + .unwrap(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_password_other(), + ) + .unwrap(); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_off_device(), + ) + .unwrap(); + + // Build + assert!(sut.validate().is_ok()); + let built = sut.build().unwrap(); + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::with_roles( + RoleWithFactorSourceIds::primary_with_factors( + 2, + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ], + [], + ), + RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + ],), + RoleWithFactorSourceIds::confirmation_with_factors([ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_off_device() + ],), + ) + ); + + pretty_assertions::assert_eq!( + built, + MatrixOfFactorSourceIds::sample_config_4_0() + ) + } + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_template.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_template.rs new file mode 100644 index 000000000..8fe804f91 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/matrix_template.rs @@ -0,0 +1,550 @@ +use crate::prelude::*; + +/// A Matrix of FactorSourceTemplates, can be used to create template +/// "SecurityShields", mostly useful for coding/tests, but theoretically +/// we could UniFFI export these and use them in the hosts wallets, which would +/// pre-populate SecurityShield-builder flow screens - if hosts/Sargon manages +/// to assign each template "slot" with a concrete FactorSourceID, known as +/// materialization. +pub type MatrixTemplate = AbstractMatrixBuilt; + +impl AbstractBuiltRoleWithFactor { + /// Tries to materialize a RoleWithFactorSourceIds from a RoleTemplate by + /// assigning each template with a concrete FactorSourceID using the FactorSourceIdAssigner. + pub(crate) fn assign( + self, + factor_source_id_assigner: &mut FactorSourceIdAssigner, + ) -> Result, CommonError> { + let mut fulfill = + |xs: &Vec| -> Result, CommonError> { + xs.iter() + .map(|f| factor_source_id_assigner.next(f)) + .collect::, CommonError>>() + }; + Ok(RoleWithFactorSourceIds::with_factors( + self.get_threshold(), + fulfill(self.get_threshold_factors())?, + fulfill(self.get_override_factors())?, + )) + } +} + +/// A helper which assigns FactorSourceIDs to FactorSourceTemplates, used for +/// materializing a MatrixTemplate into a MatrixOfFactorSourceIds. +pub(crate) struct FactorSourceIdAssigner { + factor_source_ids: Vec, + map: IndexMap, +} + +impl FactorSourceIdAssigner { + fn new( + factor_source_ids: impl IntoIterator, + ) -> Self { + Self { + factor_source_ids: factor_source_ids.into_iter().collect_vec(), + map: IndexMap::new(), + } + } + + fn next( + &mut self, + template: &FactorSourceTemplate, + ) -> Result { + if let Some(existing) = self.map.get(template) { + Ok(*existing) + } else if let Some(index_of_next) = self + .factor_source_ids + .iter() + .position(|f| f.get_factor_source_kind() == template.kind) + { + let next = self.factor_source_ids.remove(index_of_next); + self.map.insert(template.clone(), next); + Ok(next) + } else { + Err(CommonError::Unknown) + } + } +} + +impl MatrixTemplate { + pub fn materialize( + self, + factor_source_ids: impl IntoIterator, + ) -> Result { + self.materialize_ids( + factor_source_ids.into_iter().map(|f| f.factor_source_id()), + ) + } + + /// Tries to materialize a MatrixOfFactorSourceIds from a MatrixTemplate by + /// assigning each template with a concrete FactorSourceID using the `factor_source_ids`.` + pub fn materialize_ids( + self, + factor_source_ids: impl IntoIterator, + ) -> Result { + let number_of_days_until_auto_confirm = + self.number_of_days_until_auto_confirm; + + let mut assigner = FactorSourceIdAssigner::new(factor_source_ids); + + let primary_role = self.primary_role.assign(&mut assigner)?; + + let recovery_role = self.recovery_role.assign(&mut assigner)?; + + let confirmation_role = self.confirmation_role.assign(&mut assigner)?; + + let matrix = unsafe { + MatrixOfFactorSourceIds::unbuilt_with_roles_and_days( + primary_role, + recovery_role, + confirmation_role, + number_of_days_until_auto_confirm, + ) + }; + + Ok(matrix) + } +} + +impl MatrixTemplate { + fn new( + primary_role: PrimaryRoleTemplate, + recovery_role: RecoveryRoleTemplate, + confirmation_role: ConfirmationRoleTemplate, + ) -> Self { + unsafe { + Self::unbuilt_with_roles_and_days(primary_role, recovery_role, confirmation_role, MatrixOfFactorSourceIds::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM) + } + } + + /// Config 1.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_1_1() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 1.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_1_2() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::password(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 1.3 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_1_3() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::password(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 1.4 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_1_4() -> Self { + Self::new( + PrimaryRoleTemplate::new([FactorSourceTemplate::device()]), + RecoveryRoleTemplate::new([FactorSourceTemplate::ledger()]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 1.5 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_1_5() -> Self { + Self::new( + PrimaryRoleTemplate::new([FactorSourceTemplate::ledger()]), + RecoveryRoleTemplate::new([FactorSourceTemplate::device()]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 2.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_2_1() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::ledger_other(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::device()]), + ) + } + + /// Config 2.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_2_2() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::ledger_other(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::ledger_other(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::device()]), + ) + } + + /// Config 2.3 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_2_3() -> Self { + Self::new( + PrimaryRoleTemplate::new([FactorSourceTemplate::ledger()]), + RecoveryRoleTemplate::new([FactorSourceTemplate::ledger_other()]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::device()]), + ) + } + + /// Config 2.4 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_2_4() -> Self { + Self::new( + PrimaryRoleTemplate::new([FactorSourceTemplate::device()]), + RecoveryRoleTemplate::new([FactorSourceTemplate::ledger()]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::ledger_other(), + ]), + ) + } + + /// Config 3 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_3_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::ledger_other(), + ]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::password(), + ]), + ) + } + + /// Config 4 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_4_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::password(), + FactorSourceTemplate::password_other(), + FactorSourceTemplate::off_device_mnemonic(), + ]), + ) + } + + /// Config 5.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_5_1() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::password(), + ]), + RecoveryRoleTemplate::new( + [FactorSourceTemplate::trusted_contact()], + ), + ConfirmationRoleTemplate::new([FactorSourceTemplate::password()]), + ) + } + + /// Config 5.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_5_2() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::password(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::trusted_contact(), + FactorSourceTemplate::trusted_contact_other(), + FactorSourceTemplate::device(), + ]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::password(), + FactorSourceTemplate::password_other(), + FactorSourceTemplate::off_device_mnemonic(), + ]), + ) + } + + /// Config 6.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_6_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([FactorSourceTemplate::device()]), + RecoveryRoleTemplate::new( + [FactorSourceTemplate::trusted_contact()], + ), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::security_questions(), + ]), + ) + } + + /// Config 7.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_7_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::trusted_contact(), + FactorSourceTemplate::ledger(), + ]), + ConfirmationRoleTemplate::new([FactorSourceTemplate::device()]), + ) + } + + /// Config 8.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_8_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::ledger(), + FactorSourceTemplate::device(), + ]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::security_questions(), + ]), + ) + } + + /// Config 9.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn config_9_0() -> Self { + Self::new( + PrimaryRoleTemplate::new([ + FactorSourceTemplate::device(), + FactorSourceTemplate::ledger(), + ]), + RecoveryRoleTemplate::new([ + FactorSourceTemplate::trusted_contact(), + FactorSourceTemplate::device(), + ]), + ConfirmationRoleTemplate::new([ + FactorSourceTemplate::security_questions(), + ]), + ) + } +} + +#[cfg(test)] +mod test_templates { + use super::*; + + fn test_template( + template: MatrixTemplate, + expected: MatrixOfFactorSourceIds, + ) { + let m = template + .materialize_ids(*ALL_FACTOR_SOURCE_ID_SAMPLES_INC_NON_HD) + .unwrap(); + pretty_assertions::assert_eq!(m, expected); + } + + #[test] + fn template_config_1_1() { + test_template( + MatrixTemplate::config_1_1(), + MatrixOfFactorSourceIds::sample_config_1_1(), + ) + } + + #[test] + fn template_config_1_2() { + test_template( + MatrixTemplate::config_1_2(), + MatrixOfFactorSourceIds::sample_config_1_2(), + ) + } + + #[test] + fn template_config_1_3() { + test_template( + MatrixTemplate::config_1_3(), + MatrixOfFactorSourceIds::sample_config_1_3(), + ) + } + + #[test] + fn template_config_1_4() { + test_template( + MatrixTemplate::config_1_4(), + MatrixOfFactorSourceIds::sample_config_1_4(), + ) + } + + #[test] + fn template_config_1_5() { + test_template( + MatrixTemplate::config_1_5(), + MatrixOfFactorSourceIds::sample_config_1_5(), + ) + } + + #[test] + fn template_config_2_1() { + test_template( + MatrixTemplate::config_2_1(), + MatrixOfFactorSourceIds::sample_config_2_1(), + ) + } + + #[test] + fn template_config_2_2() { + test_template( + MatrixTemplate::config_2_2(), + MatrixOfFactorSourceIds::sample_config_2_2(), + ) + } + + #[test] + fn template_config_2_3() { + test_template( + MatrixTemplate::config_2_3(), + MatrixOfFactorSourceIds::sample_config_2_3(), + ) + } + + #[test] + fn template_config_2_4() { + test_template( + MatrixTemplate::config_2_4(), + MatrixOfFactorSourceIds::sample_config_2_4(), + ) + } + + #[test] + fn template_config_3_0() { + test_template( + MatrixTemplate::config_3_0(), + MatrixOfFactorSourceIds::sample_config_3_0(), + ) + } + + #[test] + fn template_config_4_0() { + test_template( + MatrixTemplate::config_4_0(), + MatrixOfFactorSourceIds::sample_config_4_0(), + ) + } + + #[test] + fn template_config_5_1() { + test_template( + MatrixTemplate::config_5_1(), + MatrixOfFactorSourceIds::sample_config_5_1(), + ) + } + + #[test] + fn template_config_5_2() { + test_template( + MatrixTemplate::config_5_2(), + MatrixOfFactorSourceIds::sample_config_5_2(), + ) + } + + #[test] + fn template_config_6_0() { + test_template( + MatrixTemplate::config_6_0(), + MatrixOfFactorSourceIds::sample_config_6_0(), + ) + } + + #[test] + fn template_config_7_0() { + test_template( + MatrixTemplate::config_7_0(), + MatrixOfFactorSourceIds::sample_config_7_0(), + ) + } + + #[test] + fn template_config_8_0() { + test_template( + MatrixTemplate::config_8_0(), + MatrixOfFactorSourceIds::sample_config_8_0(), + ) + } + + #[test] + fn template_config_9_0() { + test_template( + MatrixTemplate::config_9_0(), + MatrixOfFactorSourceIds::sample_config_9_0(), + ) + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/builder/mod.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/mod.rs new file mode 100644 index 000000000..4e87cc622 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/builder/mod.rs @@ -0,0 +1,9 @@ +mod error; +mod matrix_builder; +mod matrix_builder_unit_tests; +mod matrix_template; + +pub use error::*; +#[allow(unused_imports)] +pub use matrix_builder::*; +pub use matrix_template::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/factor_source_id_samples.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/factor_source_id_samples.rs new file mode 100644 index 000000000..0918d7ff4 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/factor_source_id_samples.rs @@ -0,0 +1,72 @@ +use crate::prelude::*; + +impl FactorSourceID { + pub fn sample_device() -> Self { + FactorSourceIDFromHash::sample_device().into() + } + pub fn sample_ledger() -> Self { + FactorSourceIDFromHash::sample_ledger().into() + } + pub fn sample_ledger_other() -> Self { + FactorSourceIDFromHash::sample_ledger_other().into() + } + pub fn sample_arculus() -> Self { + FactorSourceIDFromHash::sample_arculus().into() + } + pub fn sample_arculus_other() -> Self { + FactorSourceIDFromHash::sample_arculus_other().into() + } + + pub fn sample_password() -> Self { + FactorSourceIDFromHash::sample_password().into() + } + + pub fn sample_password_other() -> Self { + FactorSourceIDFromHash::sample_password_other().into() + } + + /// Radix Wallet (UI) calls this "passphrase" + pub fn sample_off_device() -> Self { + FactorSourceIDFromHash::sample_off_device().into() + } + /// Radix Wallet (UI) calls this "passphrase" + pub fn sample_off_device_other() -> Self { + FactorSourceIDFromHash::sample_off_device_other().into() + } + pub fn sample_security_questions() -> Self { + FactorSourceIDFromHash::sample_security_questions().into() + } + pub fn sample_device_other() -> Self { + FactorSourceIDFromHash::sample_device_other().into() + } + pub fn sample_security_questions_other() -> Self { + FactorSourceIDFromHash::sample_security_questions_other().into() + } + pub fn sample_trusted_contact() -> Self { + FactorSource::sample_trusted_contact_frank().id() + } + pub fn sample_trusted_contact_other() -> Self { + FactorSource::sample_trusted_contact_grace().id() + } +} + +#[allow(dead_code)] +pub static ALL_FACTOR_SOURCE_ID_SAMPLES_INC_NON_HD: Lazy<[FactorSourceID; 14]> = + Lazy::new(|| { + [ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger_other(), + FactorSourceID::sample_arculus(), + FactorSourceID::sample_arculus_other(), + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_off_device(), + FactorSourceID::sample_off_device_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_device_other(), + FactorSourceID::sample_security_questions_other(), + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ] + }); diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs new file mode 100644 index 000000000..6a5b15e65 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_instances.rs @@ -0,0 +1,406 @@ +use crate::prelude::*; + +pub type MatrixOfFactorInstances = AbstractMatrixBuilt; + +pub trait HasFactorInstances { + fn unique_factor_instances(&self) -> IndexSet; +} + +pub trait HasFactorSourceKindObjectSafe { + fn get_factor_source_kind(&self) -> FactorSourceKind; +} +impl HasFactorSourceKindObjectSafe for FactorSourceID { + fn get_factor_source_kind(&self) -> FactorSourceKind { + match self { + FactorSourceID::Hash { value } => value.kind, + FactorSourceID::Address { value } => value.kind, + } + } +} + +impl HasFactorInstances for MatrixOfFactorInstances { + fn unique_factor_instances(&self) -> IndexSet { + let mut set = IndexSet::new(); + set.extend(self.primary_role.all_factors().into_iter().cloned()); + set.extend(self.recovery_role.all_factors().into_iter().cloned()); + set.extend(self.confirmation_role.all_factors().into_iter().cloned()); + set + } +} + +impl MnemonicWithPassphrase { + fn derive_instances_for_factor_sources( + network_id: NetworkID, + quantity_per_factor: usize, + derivation_presets: impl IntoIterator, + sources: impl IntoIterator, + ) -> IndexMap { + let next_index_assigner = NextDerivationEntityIndexAssigner::new( + network_id, + None, + FactorInstancesCache::default(), + ); + + let derivation_presets = + derivation_presets.into_iter().collect::>(); + + sources + .into_iter() + .map(|fs| { + let fsid = fs.id_from_hash(); + let mwp = fsid.sample_associated_mnemonic(); + + let paths = derivation_presets + .clone() + .into_iter() + .map(|dp| (dp, quantity_per_factor)) + .collect::>(); + + let paths = paths + .into_iter() + .flat_map(|(derivation_preset, qty)| { + // `qty` many paths + (0..qty) + .map(|_| { + let index_agnostic_path = derivation_preset + .index_agnostic_path_on_network(network_id); + + next_index_assigner + .next(fsid, index_agnostic_path) + .map(|index| { + DerivationPath::from(( + index_agnostic_path, + index, + )) + }) + .unwrap() + }) + .collect::>() + }) + .collect::>(); + + let instances = mwp + .derive_public_keys(paths) + .into_iter() + .map(|public_key| { + HierarchicalDeterministicFactorInstance::new( + fsid, public_key, + ) + }) + .collect::(); + + (fsid, instances) + }) + .collect::>() + } +} + +impl MatrixOfFactorInstances { + fn sample_from_matrix_of_sources( + matrix_of_sources: MatrixOfFactorSources, + ) -> Self { + let mut consuming_instances = + MnemonicWithPassphrase::derive_instances_for_factor_sources( + NetworkID::Mainnet, + 1, + [DerivationPreset::AccountMfa], + matrix_of_sources.all_factors().into_iter().cloned(), + ); + + Self::fulfilling_matrix_of_factor_sources_with_instances( + &mut consuming_instances, + matrix_of_sources.clone(), + ) + .unwrap() + } +} + +impl HasSampleValues for MatrixOfFactorInstances { + fn sample() -> Self { + Self::sample_from_matrix_of_sources(MatrixOfFactorSources::sample()) + } + + fn sample_other() -> Self { + Self::sample_from_matrix_of_sources( + MatrixOfFactorSources::sample_other(), + ) + } +} + +impl MatrixOfFactorInstances { + /// Maps `MatrixOfFactorSources -> MatrixOfFactorInstances` by + /// "assigning" FactorInstances to each MatrixOfFactorInstances from + /// `consuming_instances`. + /// + /// NOTE: + /// **One FactorInstance might be used multiple times in the MatrixOfFactorInstances, + /// e.g. ones in the PrimaryRole(WithFactorInstances) and again in RecoveryRole(WithFactorInstances) or + /// in RecoveryRole(WithFactorInstances)**. + /// + /// However, the same FactorInstance is NEVER used in two different MatrixOfFactorInstances. + /// + /// + pub fn fulfilling_matrix_of_factor_sources_with_instances( + consuming_instances: &mut IndexMap< + FactorSourceIDFromHash, + FactorInstances, + >, + matrix_of_factor_sources: MatrixOfFactorSources, + ) -> Result { + let instances = &consuming_instances.clone(); + + let primary_role = + PrimaryRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + let recovery_role = + RecoveryRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + let confirmation_role = + ConfirmationRoleWithFactorInstances::fulfilling_role_of_factor_sources_with_factor_instances( + instances, + &matrix_of_factor_sources, + )?; + + let matrix = unsafe { + Self::unbuilt_with_roles_and_days( + primary_role, + recovery_role, + confirmation_role, + matrix_of_factor_sources.number_of_days_until_auto_confirm, + ) + }; + + // Now that we have assigned instances, **possibly the SAME INSTANCE to multiple roles**, + // lets delete them from the `consuming_instances` map. + for instance in matrix.all_factors() { + let fsid = + &FactorSourceIDFromHash::try_from(instance.factor_source_id) + .unwrap(); + let existing = consuming_instances.get_mut(fsid).unwrap(); + + let to_remove = HierarchicalDeterministicFactorInstance::try_from( + instance.clone(), + ) + .unwrap(); + + // We remove at the beginning of the list first. + existing.shift_remove(&to_remove); + + if existing.is_empty() { + // not needed per se, but feels prudent to "prune". + consuming_instances.shift_remove_entry(fsid); + } + } + + Ok(matrix) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + assert_ne!( + SUT::sample().unique_factor_instances(), + SUT::sample_other().unique_factor_instances() + ); + } + + #[test] + fn err_if_no_instance_found_for_factor_source() { + assert!(matches!( + SUT::fulfilling_matrix_of_factor_sources_with_instances( + &mut IndexMap::new(), + MatrixOfFactorSources::sample() + ), + Err(CommonError::MissingFactorMappingInstancesIntoRole) + )); + } + + #[test] + fn err_if_empty_instance_found_for_factor_source() { + assert!(matches!( + SUT::fulfilling_matrix_of_factor_sources_with_instances( + &mut IndexMap::kv( + FactorSource::sample_device_babylon().id_from_hash(), + FactorInstances::from_iter([]) + ), + MatrixOfFactorSources::sample() + ), + Err(CommonError::MissingFactorMappingInstancesIntoRole) + )); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "4af49eb56b1af579aaf03f1760ec526f56e2297651f7a067f4b362f685417a81" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs new file mode 100644 index 000000000..1ab5c51bb --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_source_ids.rs @@ -0,0 +1,381 @@ +use crate::prelude::*; + +pub type MatrixOfFactorSourceIds = AbstractMatrixBuilt; + +impl MatrixOfFactorSourceIds { + pub(crate) fn _unvalidated_with_roles( + primary: PrimaryRoleWithFactorSourceIds, + recovery: RecoveryRoleWithFactorSourceIds, + confirmation: ConfirmationRoleWithFactorSourceIds, + ) -> Self { + unsafe { + Self::unbuilt_with_roles_and_days( + primary, + recovery, + confirmation, + Self::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + ) + } + } +} + +#[cfg(test)] +impl MatrixOfFactorSourceIds { + pub(crate) fn with_roles_and_days( + primary: PrimaryRoleWithFactorSourceIds, + recovery: RecoveryRoleWithFactorSourceIds, + confirmation: ConfirmationRoleWithFactorSourceIds, + number_of_days_until_auto_confirm: u16, + ) -> Self { + unsafe { + Self::unbuilt_with_roles_and_days( + primary, + recovery, + confirmation, + number_of_days_until_auto_confirm, + ) + } + } + + pub(crate) fn with_roles( + primary: PrimaryRoleWithFactorSourceIds, + recovery: RecoveryRoleWithFactorSourceIds, + confirmation: ConfirmationRoleWithFactorSourceIds, + ) -> Self { + Self::with_roles_and_days( + primary, + recovery, + confirmation, + Self::DEFAULT_NUMBER_OF_DAYS_UNTIL_AUTO_CONFIRM, + ) + } +} + +impl MatrixOfFactorSourceIds { + fn sample_from_template(template: MatrixTemplate) -> Self { + template + .materialize_ids(*ALL_FACTOR_SOURCE_ID_SAMPLES_INC_NON_HD) + .unwrap() + } + + /// Config 1.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_1_1() -> Self { + Self::sample_from_template(MatrixTemplate::config_1_1()) + } + + /// Config 1.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_1_2() -> Self { + Self::sample_from_template(MatrixTemplate::config_1_2()) + } + + /// Config 1.3 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_1_3() -> Self { + Self::sample_from_template(MatrixTemplate::config_1_3()) + } + + /// Config 1.4 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_1_4() -> Self { + Self::sample_from_template(MatrixTemplate::config_1_4()) + } + + /// Config 1.5 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_1_5() -> Self { + Self::sample_from_template(MatrixTemplate::config_1_5()) + } + + /// Config 2.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_2_1() -> Self { + Self::sample_from_template(MatrixTemplate::config_2_1()) + } + + /// Config 2.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_2_2() -> Self { + Self::sample_from_template(MatrixTemplate::config_2_2()) + } + + /// Config 2.3 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_2_3() -> Self { + Self::sample_from_template(MatrixTemplate::config_2_3()) + } + + /// Config 2.4 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_2_4() -> Self { + Self::sample_from_template(MatrixTemplate::config_2_4()) + } + + /// Config 3.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_3_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_3_0()) + } + + /// Config 4.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_4_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_4_0()) + } + + /// Config 5.1 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_5_1() -> Self { + Self::sample_from_template(MatrixTemplate::config_5_1()) + } + + /// Config 5.2 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_5_2() -> Self { + Self::sample_from_template(MatrixTemplate::config_5_2()) + } + + /// Config 6.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_6_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_6_0()) + } + + /// Config 7.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_7_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_7_0()) + } + + /// Config 8.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_8_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_8_0()) + } + + /// Config 9.0 according to [this document][doc]. + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Example-Security-Shield-Configurations + pub fn sample_config_9_0() -> Self { + Self::sample_from_template(MatrixTemplate::config_9_0()) + } +} + +impl HasSampleValues for MatrixOfFactorSourceIds { + fn sample() -> Self { + Self::sample_config_1_1() + } + + fn sample_other() -> Self { + Self::sample_config_2_4() + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorSourceIds; + + #[test] + fn template() {} + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + assert_ne!(SUT::sample(), SUT::sample_config_1_2()); + assert_ne!(SUT::sample().primary(), SUT::sample_other().primary()); + assert_ne!(SUT::sample().recovery(), SUT::sample_other().recovery()); + assert_ne!( + SUT::sample().confirmation(), + SUT::sample_other().confirmation() + ); + } + + #[test] + fn hash() { + assert_eq!( + HashSet::::from_iter([ + SUT::sample_config_1_1(), + SUT::sample_config_1_2(), + SUT::sample_config_1_3(), + SUT::sample_config_1_4(), + SUT::sample_config_1_5(), + SUT::sample_config_2_1(), + SUT::sample_config_2_2(), + SUT::sample_config_2_3(), + SUT::sample_config_2_4(), + SUT::sample_config_3_0(), + SUT::sample_config_4_0(), + SUT::sample_config_5_1(), + SUT::sample_config_5_2(), + SUT::sample_config_6_0(), + SUT::sample_config_7_0(), + SUT::sample_config_8_0(), + SUT::sample_config_9_0(), + // Duplicates should be removed + SUT::sample_config_1_1(), + SUT::sample_config_1_2(), + SUT::sample_config_1_3(), + SUT::sample_config_1_4(), + SUT::sample_config_1_5(), + SUT::sample_config_2_1(), + SUT::sample_config_2_2(), + SUT::sample_config_2_3(), + SUT::sample_config_2_4(), + SUT::sample_config_3_0(), + SUT::sample_config_4_0(), + SUT::sample_config_5_1(), + SUT::sample_config_5_2(), + SUT::sample_config_6_0(), + SUT::sample_config_7_0(), + SUT::sample_config_8_0(), + SUT::sample_config_9_0(), + ]) + .len(), + 17 + ); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } + + #[test] + fn assert_json_sample_other() { + let sut = SUT::sample_other(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "primaryRole": { + "threshold": 1, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs new file mode 100644 index 000000000..eb9cbf67d --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/matrix_of_factor_sources.rs @@ -0,0 +1,72 @@ +use crate::prelude::*; + +pub type MatrixOfFactorSources = AbstractMatrixBuilt; + +impl MatrixOfFactorSources { + pub fn new( + matrix: MatrixOfFactorSourceIds, + factor_sources: &FactorSources, + ) -> Result { + let primary_role = + RoleWithFactorSources::new(matrix.primary_role, factor_sources)?; + + let recovery_role = + RoleWithFactorSources::new(matrix.recovery_role, factor_sources)?; + + let confirmation_role = RoleWithFactorSources::new( + matrix.confirmation_role, + factor_sources, + )?; + + if primary_role.role() != RoleKind::Primary + || recovery_role.role() != RoleKind::Recovery + || confirmation_role.role() != RoleKind::Confirmation + { + unreachable!("Programmer error!") + } + + let built = unsafe { + Self::unbuilt_with_roles_and_days( + primary_role, + recovery_role, + confirmation_role, + matrix.number_of_days_until_auto_confirm, + ) + }; + + Ok(built) + } +} + +impl HasSampleValues for MatrixOfFactorSources { + fn sample() -> Self { + let ids = MatrixOfFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = MatrixOfFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = MatrixOfFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/matrices/mod.rs b/crates/sargon/src/profile/mfa/security_structures/matrices/mod.rs new file mode 100644 index 000000000..e5e1338a6 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/matrices/mod.rs @@ -0,0 +1,14 @@ +mod abstract_matrix_builder_or_built; +mod builder; +mod factor_source_id_samples; +mod matrix_of_factor_instances; +mod matrix_of_factor_source_ids; +mod matrix_of_factor_sources; + +pub(crate) use abstract_matrix_builder_or_built::*; +#[allow(unused_imports)] +pub use builder::*; +pub use factor_source_id_samples::*; +pub use matrix_of_factor_instances::*; +pub use matrix_of_factor_source_ids::*; +pub use matrix_of_factor_sources::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/mod.rs b/crates/sargon/src/profile/mfa/security_structures/mod.rs index b3941560c..3c90ae55f 100644 --- a/crates/sargon/src/profile/mfa/security_structures/mod.rs +++ b/crates/sargon/src/profile/mfa/security_structures/mod.rs @@ -1,15 +1,19 @@ -mod decl_security_structure_of; -mod factor_instance_level; -mod factor_source_id_level; -mod factor_source_level; mod has_role_kind; -mod role_with_factors; +mod matrices; +mod roles; +mod security_shield_builder; +mod security_shield_builder_invalid_reason; +mod security_shield_prerequisites_status; +mod security_structure_id; mod security_structure_metadata; +mod security_structure_of_factors; -pub(crate) use decl_security_structure_of::*; -pub use factor_instance_level::*; -pub use factor_source_id_level::*; -pub use factor_source_level::*; pub use has_role_kind::*; -pub use role_with_factors::*; +pub use matrices::*; +pub use roles::*; +pub use security_shield_builder::*; +pub use security_shield_builder_invalid_reason::*; +pub use security_shield_prerequisites_status::*; +pub use security_structure_id::*; pub use security_structure_metadata::*; +pub use security_structure_of_factors::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/role_with_factors.rs b/crates/sargon/src/profile/mfa/security_structures/role_with_factors.rs deleted file mode 100644 index f7faa1a73..000000000 --- a/crates/sargon/src/profile/mfa/security_structures/role_with_factors.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::prelude::*; - -pub trait RoleWithFactors { - fn get_threshold_factors(&self) -> &Vec; - fn get_threshold(&self) -> u8; - fn get_override_factors(&self) -> &Vec; - - fn all_factors(&self) -> IndexSet<&Factor> { - let mut factors = - IndexSet::from_iter(self.get_threshold_factors().iter()); - factors.extend(self.get_override_factors().iter()); - factors - } -} - -pub trait HasFactorInstances { - fn unique_factor_instances(&self) -> IndexSet; -} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/abstract_role_builder_or_built.rs b/crates/sargon/src/profile/mfa/security_structures/roles/abstract_role_builder_or_built.rs new file mode 100644 index 000000000..edd52ad74 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/abstract_role_builder_or_built.rs @@ -0,0 +1,216 @@ +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +use crate::prelude::*; + +/// Either a role or a **builder of a role** with a threshold, threshold_factors and override_factors. +/// This type is shared by: +/// # Builder +/// * PrimaryRoleBuilder (FactorSourceID) +/// * RecoveryRoleBuilder (FactorSourceID) +/// * ConfirmationRoleBuilder (FactorSourceID) +/// +/// # Built +/// +/// ## FactorSourceID +/// * PrimaryRoleWithFactorSourceID +/// * RecoveryRoleWithFactorSourceID +/// * ConfirmationRoleWithFactorSourceID +/// +/// ## FactorSource +/// * PrimaryRoleWithFactorSource +/// * RecoveryRoleWithFactorSource +/// * ConfirmationRoleWithFactorSource +/// +/// ## FactorInstance +/// * PrimaryRoleWithFactorInstances +/// * RecoveryRoleWithFactorInstances +/// * ConfirmationRoleWithFactorInstances +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractRoleBuilderOrBuilt { + /// How many threshold factors that must be used to perform some function with + /// this role. + threshold: u8, + + /// Factors which are used in combination with other factors, amounting to at + /// least `threshold` many factors to perform some function with this role. + threshold_factors: Vec, + + /// Overriding / Super admin / "sudo" / God / factors, **ANY** + /// single of these factor which can perform the function of this role, + /// disregarding of `threshold`. + override_factors: Vec, +} + +impl RoleBuilder +where + Assert<{ ROLE > ROLE_PRIMARY }>: IsTrue, +{ + /// Removes all override factors from this role + pub fn reset(&mut self) { + self.override_factors.clear(); + + // This is not necessary, but why not... + self.threshold_factors.clear(); + self.threshold = 0; + } +} + +pub(crate) type AbstractBuiltRoleWithFactor = + AbstractRoleBuilderOrBuilt; + +pub(crate) type RoleBuilder = + AbstractRoleBuilderOrBuilt; + +impl + AbstractRoleBuilderOrBuilt +{ + pub fn role(&self) -> RoleKind { + RoleKind::from_u8(ROLE).expect("RoleKind should be valid") + } + + /// # Safety + /// Rust memory safe, but marked "unsafe" since it might allow for instantiation + /// of unsafe - as in application **unsecure** - Role of Factors, which might + /// lead to increase risk for end user to loose funds. + pub unsafe fn unbuilt_with_factors( + threshold: u8, + threshold_factors: impl IntoIterator, + override_factors: impl IntoIterator, + ) -> Self { + let assert_is_securified = + |factors: &Vec| -> Result<(), CommonError> { + let trait_objects: Vec<&dyn IsMaybeKeySpaceAware> = factors + .iter() + .map(|x| x as &dyn IsMaybeKeySpaceAware) + .collect(); + if trait_objects + .iter() + .filter_map(|x| x.maybe_key_space()) + .any(|x| x != KeySpace::Securified) + { + return Err( + crate::CommonError::IndexUnsecurifiedExpectedSecurified, + ); + } + Ok(()) + }; + + let threshold_factors = threshold_factors.into_iter().collect(); + let override_factors = override_factors.into_iter().collect(); + + assert_is_securified(&threshold_factors) + .expect("Should not have allowed building of invalid Role"); + assert_is_securified(&override_factors) + .expect("Should not have allowed building of invalid Role"); + + Self { + threshold, + threshold_factors, + override_factors, + } + } + + pub(crate) fn with_factors( + threshold: u8, + threshold_factors: impl IntoIterator, + override_factors: impl IntoIterator, + ) -> Self { + unsafe { + Self::unbuilt_with_factors( + threshold, + threshold_factors, + override_factors, + ) + } + } +} + +impl + AbstractRoleBuilderOrBuilt +{ + /// Threshold and Override factors mixed (threshold first). + pub fn all_factors(&self) -> Vec<&FACTOR> { + self.threshold_factors + .iter() + .chain(self.override_factors.iter()) + .collect() + } + + /// Factors which are used in combination with other factors, amounting to at + /// least `threshold` many factors to perform some function with this role. + pub fn get_threshold_factors(&self) -> &Vec { + &self.threshold_factors + } + + /// Overriding / Super admin / "sudo" / God / factors, **ANY** + /// single of these factor which can perform the function of this role, + /// disregarding of `threshold`. + pub fn get_override_factors(&self) -> &Vec { + &self.override_factors + } + + /// How many threshold factors that must be used to perform some function with + /// this role. + pub fn get_threshold(&self) -> u8 { + self.threshold + } +} +pub(crate) const ROLE_PRIMARY: u8 = 1; +pub(crate) const ROLE_RECOVERY: u8 = 2; +pub(crate) const ROLE_CONFIRMATION: u8 = 3; + +pub(crate) trait RoleFromDiscriminator { + fn from_u8(discriminator: u8) -> Option + where + Self: Sized; +} +impl RoleFromDiscriminator for RoleKind { + fn from_u8(discriminator: u8) -> Option { + match discriminator { + ROLE_PRIMARY => Some(RoleKind::Primary), + ROLE_RECOVERY => Some(RoleKind::Recovery), + ROLE_CONFIRMATION => Some(RoleKind::Confirmation), + _ => None, + } + } +} + +impl RoleBuilder { + pub(crate) fn new() -> Self { + Self { + threshold: 0, + threshold_factors: Vec::new(), + override_factors: Vec::new(), + } + } + + pub(crate) fn mut_threshold_factors(&mut self) -> &mut Vec { + &mut self.threshold_factors + } + + pub(crate) fn mut_override_factors(&mut self) -> &mut Vec { + &mut self.override_factors + } + + pub(crate) fn unchecked_add_factor_source_to_list( + &mut self, + factor_source_id: FactorSourceID, + factor_list_kind: FactorListKind, + ) { + match factor_list_kind { + FactorListKind::Threshold => { + self.threshold_factors.push(factor_source_id) + } + FactorListKind::Override => { + self.override_factors.push(factor_source_id) + } + } + } + + pub(crate) fn unchecked_set_threshold(&mut self, threshold: u8) { + self.threshold = threshold; + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/confirmation_roles_builder_unit_tests.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/confirmation_roles_builder_unit_tests.rs new file mode 100644 index 000000000..a7b9bd2cd --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/confirmation_roles_builder_unit_tests.rs @@ -0,0 +1,345 @@ +#![cfg(test)] + +use crate::prelude::*; + +#[allow(clippy::upper_case_acronyms)] + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_confirmation() { + assert_eq!( + ConfirmationRoleBuilder::new().role(), + RoleKind::Confirmation + ); +} + +#[test] +fn empty_is_err_confirmation() { + let sut = ConfirmationRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = ConfirmationRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[test] +fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + let sut = make(); + let not_ok = |kind: FactorSourceKind| { + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + let ok = |kind: FactorSourceKind| { + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + ok(Device); + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(SecurityQuestions); + ok(Password); + ok(OffDeviceMnemonic); + not_ok(TrustedContact); +} + +mod device_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn set_threshold_is_unsupported() { + let mut sut = make(); + assert_eq!( + sut.set_threshold(1), + MutRes::basic_violation( + BasicViolation::ConfirmationCannotSetThreshold + ) + ); + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + let built = sut.build().unwrap(); + assert!(built.get_threshold_factors().is_empty()); + assert_eq!( + built, + RoleWithFactorSourceIds::confirmation_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod ledger_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod arculus_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod off_device_mnemonic_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_off_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_off_device_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod trusted_contact_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_trusted_contact() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ); + } +} + +mod password_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_password_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::confirmation_with_factors([ + sample(), + sample_other() + ]) + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/mod.rs new file mode 100644 index 000000000..af9dc07f3 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/mod.rs @@ -0,0 +1,7 @@ +mod confirmation_roles_builder_unit_tests; +mod primary_roles_builder_unit_tests; +mod recovery_roles_builder_unit_tests; +mod roles_builder; +mod roles_builder_unit_tests; + +pub use roles_builder::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/primary_roles_builder_unit_tests.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/primary_roles_builder_unit_tests.rs new file mode 100644 index 000000000..012ea265c --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/primary_roles_builder_unit_tests.rs @@ -0,0 +1,965 @@ +#![cfg(test)] + +use crate::prelude::*; + +use NotYetValidReason::*; +type Validation = RoleBuilderValidation; + +#[allow(clippy::upper_case_acronyms)] + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_primary() { + assert_eq!(PrimaryRoleBuilder::new().role(), RoleKind::Primary); +} + +#[test] +fn empty_is_err_primary() { + let sut = PrimaryRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +mod primary_test_helper_functions { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleBuilder; + + #[test] + fn factor_sources_not_of_kind_to_list_of_kind_in_override() { + let mut sut = SUT::new(); + sut.add_factor_source_to_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_arculus()) + .unwrap(); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::Device, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::LedgerHQHardwareWallet, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::ArculusCard, + FactorListKind::Override, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ] + ); + } + + #[test] + fn factor_sources_not_of_kind_to_list_of_kind_in_threshold() { + let mut sut = SUT::new(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_arculus()) + .unwrap(); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::Device, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::LedgerHQHardwareWallet, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_arculus() + ] + ); + + let xs = sut.factor_sources_not_of_kind_to_list_of_kind( + FactorSourceKind::ArculusCard, + FactorListKind::Threshold, + ); + assert_eq!( + xs, + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_ledger() + ] + ); + } +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = PrimaryRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +#[cfg(test)] +mod threshold_suite { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_third() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + #[test] + fn remove_lowers_threshold_from_1_to_0() { + let mut sut = make(); + let fs = sample(); + assert_eq!(sut.get_threshold(), 0); + sut.add_factor_source_to_threshold(fs).unwrap(); // should automatically increase threshold to 1 + assert_eq!(sut.get_threshold(), 1); + sut.remove_factor_source(&fs).unwrap(); + assert_eq!(sut.get_threshold(), 0); + } + + #[test] + fn remove_lowers_threshold_from_3_to_1() { + let mut sut = make(); + let fs0 = sample(); + let fs1 = sample_other(); + sut.add_factor_source_to_threshold(fs0).unwrap(); + sut.add_factor_source_to_threshold(fs1).unwrap(); + sut.add_factor_source_to_threshold( + FactorSourceID::sample_arculus_other(), + ) + .unwrap(); + sut.set_threshold(2).unwrap(); + assert_eq!(sut.get_threshold(), 2); + sut.remove_factor_source(&fs0).unwrap(); + assert_eq!(sut.get_threshold(), 2); // assert that we DIDN'T lower the threshold, since we have 2 factors + sut.remove_factor_source(&fs1).unwrap(); + assert_eq!(sut.get_threshold(), 1); // assert that we DID lower the threshold now that we have 1 factor + } + + #[test] + fn remove_from_override_does_not_change_threshold() { + let mut sut = make(); + sut.add_factor_source_to_threshold(sample()).unwrap(); + let _ = sut.build(); // build should not mutate neither consume + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + let fs = FactorSourceID::sample_arculus_other(); + sut.add_factor_source_to_override(fs).unwrap(); + let _ = sut.build(); // build should not mutate neither consume + sut.set_threshold(2).unwrap(); + let _ = sut.build(); // build should not mutate neither consume + assert_eq!(sut.get_threshold(), 2); + sut.remove_factor_source(&fs).unwrap(); + assert_eq!(sut.get_threshold(), 2); + + let built = sut.build().unwrap(); + let built2 = sut.build().unwrap(); + assert_eq!(built.get_threshold(), 2); + assert_eq!(built2, built); // can built many times + + assert_eq!(built.role(), RoleKind::Primary); + + assert_eq!( + built.get_threshold_factors(), + &vec![sample(), sample_other()] + ); + + assert_eq!(built.get_override_factors(), &Vec::new()); + } + + #[test] + fn one_factor_then_set_threshold_to_one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn zero_factor_then_set_threshold_to_one_is_not_yet_valid_then_add_one_factor_is_ok( + ) { + // Arrange + let mut sut = make(); + + // Act + assert_eq!( + sut.set_threshold(1), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn zero_factor_then_set_threshold_to_two_is_not_yet_valid_then_add_two_factor_is_ok( + ) { + // Arrange + let mut sut = make(); + + // Act + assert_eq!( + sut.set_threshold(2), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + sut.add_factor_source_to_threshold(sample()).unwrap(); + + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 2, + [sample(), sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn add_two_factors_then_set_threshold_to_two_is_ok() { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Act + assert_eq!(sut.set_threshold(2), Ok(())); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 2, + [sample(), sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn add_two_factors_then_set_threshold_to_three_is_not_yet_valid_then_add_third_factor_is_ok( + ) { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + + // Act + assert_eq!( + sut.set_threshold(3), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + + sut.add_factor_source_to_threshold(sample_third()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 3, + [sample(), sample_other(), sample_third()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_factors_set_threshold_of_one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_override_factors_set_threshold_to_one_is_not_yet_valid() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample_other()).unwrap(); + assert_eq!( + sut.set_threshold(1), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + + // Assert + + assert_eq!( + sut.build(), + Err(Validation::NotYetValid( + ThresholdHigherThanThresholdFactorsLen + )) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_for_each_before_after_adding_a_factor( + ) { + let mut sut = make(); + let fs0 = FactorSourceID::sample_ledger(); + let fs1 = FactorSourceID::sample_password(); + let fs2 = FactorSourceID::sample_arculus(); + let xs = sut.validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + &IndexSet::from_iter([fs0, fs1, fs2]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs0,), + FactorSourceInRoleBuilderValidationStatus::not_yet_valid( + RoleKind::Primary, + fs1, + NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ), + FactorSourceInRoleBuilderValidationStatus::ok(RoleKind::Primary, fs2,), + ] + ); + _ = sut.add_factor_source_to_threshold(fs0); + _ = sut.set_threshold(2); + + let xs = sut.validation_for_addition_of_factor_source_for_each( + FactorListKind::Threshold, + &IndexSet::from_iter([fs0, fs1, fs2]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + fs0, + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + fs1, + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Primary, + fs2, + ), + ] + ); + } +} + +#[cfg(test)] +mod password { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_password_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + + #[test] + fn duplicates_not_allowed() { + let mut sut = make(); + sut.add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + _ = sut.set_threshold(2); + test_duplicates_not_allowed( + sut, + FactorListKind::Threshold, + sample(), + ); + } + + #[test] + fn alone_is_not_ok() { + // Arrange + let mut sut = make(); + let _ = sut.set_threshold(1); + // Act + let res = sut.add_factor_source_to_threshold(sample()); + + // Assert + assert_eq!( + res, + MutRes::not_yet_valid( + NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ) + ); + + let validation = sut.validate(); + assert_eq!( + validation, + Result::not_yet_valid( + NotYetValidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + + let not_ok = |kind: FactorSourceKind| { + let sut = make(); + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_list( + kind, + FactorListKind::Threshold, + ); + assert!(res.is_err()); + }; + + let ok_with = |kind: FactorSourceKind, setup: fn(&mut SUT)| { + let mut sut = make(); + setup(&mut sut); + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_list( + kind, + FactorListKind::Threshold, + ); + assert!(res.is_ok()); + }; + let ok = |kind: FactorSourceKind| { + ok_with(kind, |_| {}); + }; + + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(OffDeviceMnemonic); + + ok_with(Device, |sut| { + sut.add_factor_source_to_threshold( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + }); + ok_with(Password, |sut| { + sut.add_factor_source_to_threshold( + FactorSourceID::sample_device(), + ) + .unwrap(); + _ = sut.set_threshold(2); + }); + + not_ok(SecurityQuestions); + not_ok(TrustedContact); + } + } + + mod override_in_isolation { + use super::*; + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source_to_override(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::PrimaryCannotHavePasswordInOverrideList + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + sut.add_factor_source_to_override(FactorSourceID::sample_device()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source_to_override(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source_to_override(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::PrimaryCannotHavePasswordInOverrideList + ) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_of_kind_to_override() { + use FactorSourceKind::*; + + let not_ok = |kind: FactorSourceKind| { + let sut = make(); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + + let ok_with = |kind: FactorSourceKind, setup: fn(&mut SUT)| { + let mut sut = make(); + setup(&mut sut); + let res = sut.validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + let ok = |kind: FactorSourceKind| { + ok_with(kind, |_| {}); + }; + + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(OffDeviceMnemonic); + + ok_with(Device, |sut| { + sut.add_factor_source_to_override( + FactorSourceID::sample_ledger(), + ) + .unwrap(); + }); + + not_ok(Password); + + not_ok(SecurityQuestions); + not_ok(TrustedContact); + } + } +} + +#[cfg(test)] +mod ledger { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn one_with_threshold_of_zero_is_err() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); // should automatically bump threshold to 1 + + let _ = sut.set_threshold(0); + + // Assert + assert_eq!( + sut.build(), + Err(RoleBuilderValidation::NotYetValid( + NotYetValidReason::PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero + )) + ); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(2).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 2, + [sample(), sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + } + + mod override_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [sample()], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + sut.add_factor_source_to_override(sample_other()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [sample(), sample_other()], + ); + assert_eq!(sut.build().unwrap(), expected); + } + } +} + +#[cfg(test)] +mod arculus { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + mod threshold_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.add_factor_source_to_threshold(sample_other()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample(), sample_other()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + } + + mod override_in_isolation { + use super::*; + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()); + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [sample()], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + sut.add_factor_source_to_override(sample_other()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [sample(), sample_other()], + ); + assert_eq!(sut.build().unwrap(), expected); + } + } +} + +#[cfg(test)] +mod device_factor_source { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn test_suite_prerequisite() { + assert_eq!(sample(), sample()); + assert_eq!(sample_other(), sample_other()); + assert_ne!(sample(), sample_other()); + } + + #[cfg(test)] + mod threshold_in_isolation { + use super::*; + + fn list() -> FactorListKind { + FactorListKind::Threshold + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()) + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_threshold(sample()).unwrap(); + sut.set_threshold(1).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 1, + [sample()], + [], + ); + assert_eq!(sut.build().unwrap(), expected); + } + + #[test] + fn two_different_is_err() { + // Arrange + let mut sut = make(); + + sut.add_factor_source_to_threshold(sample()).unwrap(); + + // Act + let res = sut.add_factor_source_to_threshold(sample_other()); + + // Assert + assert!(matches!( + res, + MutRes::Err(Validation::ForeverInvalid( + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + )) + )); + } + } + + mod override_in_isolation { + + use super::*; + + fn list() -> FactorListKind { + FactorListKind::Override + } + + #[test] + fn duplicates_not_allowed() { + test_duplicates_not_allowed(make(), list(), sample()) + } + + #[test] + fn one_is_ok() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source_to_override(sample()).unwrap(); + + // Assert + let expected = RoleWithFactorSourceIds::primary_with_factors( + 0, + [], + [sample()], + ); + assert_eq!(sut.build().unwrap(), expected); + } + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/recovery_roles_builder_unit_tests.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/recovery_roles_builder_unit_tests.rs new file mode 100644 index 000000000..8a27eec1a --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/recovery_roles_builder_unit_tests.rs @@ -0,0 +1,421 @@ +#![cfg(test)] + +use crate::prelude::*; + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn new_builder_recovery() { + assert_eq!(RecoveryRoleBuilder::new().role(), RoleKind::Recovery); +} + +#[test] +fn empty_is_err_recovery() { + let sut = RecoveryRoleBuilder::new(); + let res = sut.build(); + assert_eq!( + res, + Result::not_yet_valid(NotYetValidReason::RoleMustHaveAtLeastOneFactor) + ); +} + +#[allow(clippy::upper_case_acronyms)] +type SUT = RecoveryRoleBuilder; + +fn make() -> SUT { + SUT::new() +} + +fn list() -> FactorListKind { + FactorListKind::Override +} + +#[test] +fn validation_for_addition_of_factor_source_of_kind_to_list() { + use FactorSourceKind::*; + let sut = make(); + let not_ok = |kind: FactorSourceKind| { + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_err()); + }; + let ok = |kind: FactorSourceKind| { + let res = sut + .validation_for_addition_of_factor_source_of_kind_to_override(kind); + assert!(res.is_ok()); + }; + ok(Device); + ok(LedgerHQHardwareWallet); + ok(ArculusCard); + ok(TrustedContact); + ok(OffDeviceMnemonic); + + not_ok(Password); + not_ok(SecurityQuestions); +} + +#[test] +fn set_threshold_is_unsupported() { + let mut sut = make(); + assert_eq!( + sut.set_threshold(1), + MutRes::basic_violation(BasicViolation::RecoveryCannotSetThreshold) + ); +} + +mod device_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_device_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([ + sample(), + sample_other() + ],) + ); + } + + #[test] + fn validation_for_addition_of_factor_source_for_each() { + let sut = make(); + let xs = sut.validation_for_addition_of_factor_source_for_each( + list(), + &IndexSet::from_iter([sample(), sample_other()]), + ); + assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Recovery, + sample() + ), + FactorSourceInRoleBuilderValidationStatus::ok( + RoleKind::Recovery, + sample_other(), + ) + ] + ); + } +} + +mod ledger_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_ledger() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_ledger_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()],) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod arculus_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_arculus() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_arculus_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod off_device_mnemonic_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_off_device() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_off_device_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample()]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod trusted_contact_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_trusted_contact() + } + + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_trusted_contact_other() + } + + #[test] + fn allowed_as_first_and_only() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([sample(),]) + ); + } + + #[test] + fn two_of_same_kind_allowed() { + // Arrange + let mut sut = make(); + + // Act + sut.add_factor_source(sample()).unwrap(); + sut.add_factor_source(sample_other()).unwrap(); + + // Assert + assert_eq!( + sut.build().unwrap(), + RoleWithFactorSourceIds::recovery_with_factors([ + sample(), + sample_other() + ]) + ); + } +} + +mod password_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_password() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::RecoveryRolePasswordNotSupported + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::RecoveryRolePasswordNotSupported + ) + ); + } +} + +mod security_questions_in_isolation { + use super::*; + + fn sample() -> FactorSourceID { + FactorSourceID::sample_security_questions() + } + fn sample_other() -> FactorSourceID { + FactorSourceID::sample_security_questions_other() + } + + #[test] + fn unsupported() { + // Arrange + let mut sut = make(); + + // Act + let res = sut.add_factor_source(sample()); + + // Assert + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + ) + ); + } + + #[test] + fn valid_then_invalid_because_unsupported() { + // Arrange + let mut sut = make(); + + sut.add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + sut.add_factor_source(FactorSourceID::sample_arculus()) + .unwrap(); + + // Act + let res = sut.add_factor_source(sample_other()); + + // Assert + let reason = + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported; + let err = MutRes::forever_invalid(reason); + assert_eq!(res, err); + + // .. erroneous action above did not change the state of the builder (SUT), + // so we can build and `sample` is not present in the built result. + assert_eq!( + sut.build(), + Ok(RoleWithFactorSourceIds::recovery_with_factors([ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_arculus() + ])) + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder.rs new file mode 100644 index 000000000..6a9fde824 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder.rs @@ -0,0 +1,882 @@ +use crate::prelude::*; + +use FactorListKind::*; + +pub type PrimaryRoleBuilder = RoleBuilder<{ ROLE_PRIMARY }>; +pub type RecoveryRoleBuilder = RoleBuilder<{ ROLE_RECOVERY }>; +pub type ConfirmationRoleBuilder = RoleBuilder<{ ROLE_CONFIRMATION }>; + +#[cfg(test)] +impl PrimaryRoleWithFactorSourceIds { + pub(crate) fn primary_with_factors( + threshold: u8, + threshold_factors: impl IntoIterator, + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(threshold, threshold_factors, override_factors) + } +} + +#[cfg(test)] +impl RecoveryRoleWithFactorSourceIds { + pub(crate) fn recovery_with_factors( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, vec![], override_factors) + } +} + +#[cfg(test)] +impl ConfirmationRoleWithFactorSourceIds { + pub(crate) fn confirmation_with_factors( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, vec![], override_factors) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum RoleBuilderValidation { + #[error("Basic violation: {0}")] + BasicViolation(#[from] BasicViolation), + + #[error("Forever invalid: {0}")] + ForeverInvalid(#[from] ForeverInvalidReason), + + #[error("Not yet valid: {0}")] + NotYetValid(#[from] NotYetValidReason), +} +use RoleBuilderValidation::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum BasicViolation { + /// e.g. tried to remove a factor source which was not found. + #[error("FactorSourceID not found")] + FactorSourceNotFound, + + #[error("Recovery cannot set threshold")] + RecoveryCannotSetThreshold, + + #[error("Confirmation cannot set threshold")] + ConfirmationCannotSetThreshold, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum NotYetValidReason { + #[error("Role must have at least one factor")] + RoleMustHaveAtLeastOneFactor, + + #[error( + "Primary role with password in threshold list must have another factor" + )] + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + + #[error( + "Primary role with threshold factors cannot have a threshold of zero" + )] + PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero, + + #[error("Primary role with password in threshold list must have threshold greater than one")] + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + + #[error("Threshold higher than threshold factors len")] + ThresholdHigherThanThresholdFactorsLen, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)] +pub enum ForeverInvalidReason { + #[error("Factor source already present")] + FactorSourceAlreadyPresent, + + #[error("Primary role cannot have multiple devices")] + PrimaryCannotHaveMultipleDevices, + + #[error("Primary role cannot have password in override list")] + PrimaryCannotHavePasswordInOverrideList, + + #[error("Primary role cannot contain Security Questions")] + PrimaryCannotContainSecurityQuestions, + + #[error("Primary role cannot contain Trusted Contact")] + PrimaryCannotContainTrustedContact, + + #[error("Recovery role Security Questions not supported")] + RecoveryRoleSecurityQuestionsNotSupported, + + #[error("Recovery role password not supported")] + RecoveryRolePasswordNotSupported, + + #[error("Confirmation role cannot contain Trusted Contact")] + ConfirmationRoleTrustedContactNotSupported, +} + +pub(crate) trait FromForeverInvalid { + fn forever_invalid(reason: ForeverInvalidReason) -> Self; +} +impl FromForeverInvalid for std::result::Result { + fn forever_invalid(reason: ForeverInvalidReason) -> Self { + Err(ForeverInvalid(reason)) + } +} + +pub(crate) trait FromNotYetValid { + fn not_yet_valid(reason: NotYetValidReason) -> Self; +} +impl FromNotYetValid for std::result::Result { + fn not_yet_valid(reason: NotYetValidReason) -> Self { + Err(NotYetValid(reason)) + } +} + +pub(crate) trait FromBasicViolation { + fn basic_violation(reason: BasicViolation) -> Self; +} +impl FromBasicViolation for std::result::Result { + fn basic_violation(reason: BasicViolation) -> Self { + Err(BasicViolation(reason)) + } +} + +impl BasicViolation { + pub(crate) fn threshold_list_not_supported_for_role( + role: RoleKind, + ) -> Self { + match role { + RoleKind::Recovery => Self::RecoveryCannotSetThreshold, + RoleKind::Confirmation => Self::ConfirmationCannotSetThreshold, + RoleKind::Primary => { + unreachable!("Primary role DOES support threshold list. This is programmer error.") + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FactorSourceInRoleBuilderValidationStatus { + pub role: RoleKind, + pub factor_source_id: FactorSourceID, + pub validation: RoleBuilderMutateResult, +} + +impl FactorSourceInRoleBuilderValidationStatus { + pub(crate) fn new( + role: RoleKind, + factor_source_id: FactorSourceID, + validation: RoleBuilderMutateResult, + ) -> Self { + Self { + role, + factor_source_id, + validation, + } + } +} + +#[cfg(test)] +impl FactorSourceInRoleBuilderValidationStatus { + pub(crate) fn ok(role: RoleKind, factor_source_id: FactorSourceID) -> Self { + Self::new(role, factor_source_id, Ok(())) + } + + pub(crate) fn forever_invalid( + role: RoleKind, + factor_source_id: FactorSourceID, + reason: ForeverInvalidReason, + ) -> Self { + Self::new( + role, + factor_source_id, + RoleBuilderMutateResult::forever_invalid(reason), + ) + } + + pub(crate) fn not_yet_valid( + role: RoleKind, + factor_source_id: FactorSourceID, + reason: NotYetValidReason, + ) -> Self { + Self::new( + role, + factor_source_id, + RoleBuilderMutateResult::not_yet_valid(reason), + ) + } +} + +use BasicViolation::*; +use ForeverInvalidReason::*; +use NotYetValidReason::*; +use RoleKind::*; + +pub type RoleBuilderMutateResult = Result<(), RoleBuilderValidation>; + +pub enum Assert {} +pub trait IsTrue {} +impl IsTrue for Assert {} + +impl RoleBuilder +where + Assert<{ ROLE == ROLE_PRIMARY }>: IsTrue, +{ + /// ```ignore + /// If Ok | Err(NotYetValid) => self is mutated + /// If Err(ForeverInvalid) => self is NOT mutated + /// ``` + /// + /// Also sets the threshold to 1 this is the first factor set and if + /// the threshold was 0. + pub(crate) fn add_factor_source_to_threshold( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + let should_set_threshold_to_one = self.get_threshold() == 0 + && self.get_threshold_factors().is_empty(); + self._add_factor_source_to_list(factor_source_id, Threshold) + .inspect(|_| { + if should_set_threshold_to_one { + let _ = self.set_threshold(1); + } + }) + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `Threshold` + /// what would be the validation status? + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, Threshold) + } + + #[cfg(test)] + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_list( + &self, + factor_source_kind: FactorSourceKind, + list: FactorListKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, list) + } +} + +impl RoleBuilder +where + Assert<{ ROLE > ROLE_PRIMARY }>: IsTrue, +{ + /// ```ignore + /// Ok | Err(NotYetValid) => self is mutated + /// Err(ForeverInvalid) => self is NOT mutated + /// ``` + pub(crate) fn add_factor_source( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + self.add_factor_source_to_override(factor_source_id) + } +} + +impl RoleBuilder { + /// ```ignore + /// Ok | Err(NotYetValid) => self is mutated + /// Err(ForeverInvalid) => self is NOT mutated + /// ``` + pub(crate) fn add_factor_source_to_override( + &mut self, + factor_source_id: FactorSourceID, + ) -> RoleBuilderMutateResult { + self._add_factor_source_to_list(factor_source_id, Override) + } + + /// ```ignore + /// Ok | Err(NotYetValid) => self is mutated + /// Err(ForeverInvalid) => self is NOT mutated + /// ``` + fn _add_factor_source_to_list( + &mut self, + factor_source_id: FactorSourceID, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + let validation = self.validation_for_addition_of_factor_source_to_list( + &factor_source_id, + factor_list_kind, + ); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => { + self.unchecked_add_factor_source_to_list( + factor_source_id, + factor_list_kind, + ); + } + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => {} + } + validation + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `Override` + /// what would be the validation status? + pub(crate) fn validation_for_addition_of_factor_source_of_kind_to_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self._validation_add(factor_source_kind, Override) + } + + /// If we would add a factor of kind `factor_source_kind` to the list of kind `factor_list_kind` + /// what would be the validation status? + fn _validation_add( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + match self.role() { + RoleKind::Primary => { + self.validation_for_addition_of_factor_source_of_kind_to_list_for_primary( + factor_source_kind, + factor_list_kind, + ) + } + RoleKind::Recovery | RoleKind::Confirmation => match factor_list_kind { + Threshold => { + Result::basic_violation( + BasicViolation::threshold_list_not_supported_for_role(self.role()), + ) + } + Override => { + self.validation_for_addition_of_factor_source_of_kind_to_override_for_non_primary_role( + factor_source_kind, + ) + } + }, + } + } +} + +impl RoleBuilder { + pub(crate) fn build( + &self, + ) -> Result, RoleBuilderValidation> { + self.validate().map(|_| { + RoleWithFactorSourceIds::with_factors( + self.get_threshold(), + self.get_threshold_factors().clone(), + self.get_override_factors().clone(), + ) + }) + } + + pub(crate) fn set_threshold( + &mut self, + threshold: u8, + ) -> RoleBuilderMutateResult { + match self.role() { + Primary => { + self.unchecked_set_threshold(threshold); + self.validate() + } + Recovery => RoleBuilderMutateResult::basic_violation( + RecoveryCannotSetThreshold, + ), + Confirmation => RoleBuilderMutateResult::basic_violation( + ConfirmationCannotSetThreshold, + ), + } + } + + fn override_contains_factor_source( + &self, + factor_source_id: &FactorSourceID, + ) -> bool { + self.get_override_factors().contains(factor_source_id) + } + + fn threshold_contains_factor_source( + &self, + factor_source_id: &FactorSourceID, + ) -> bool { + self.get_threshold_factors().contains(factor_source_id) + } + + fn override_contains_factor_source_of_kind( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get_override_factors() + .iter() + .any(|f| f.get_factor_source_kind() == factor_source_kind) + } + + fn threshold_contains_factor_source_of_kind( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.get_threshold_factors() + .iter() + .any(|f| f.get_factor_source_kind() == factor_source_kind) + } + + pub(crate) fn check_threshold_for_primary( + &self, + ) -> Option { + if self.get_threshold_factors().len() < self.get_threshold() as usize { + return Some( + NotYetValidReason::ThresholdHigherThanThresholdFactorsLen, + ); + } + if self.get_threshold() == 0 && !self.get_threshold_factors().is_empty() + { + return Some( + NotYetValidReason::PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero, + ); + } + None + } + + pub(crate) fn validate_threshold_for_primary( + &self, + ) -> RoleBuilderMutateResult { + if let Some(not_yet_valid) = self.check_threshold_for_primary() { + return Err(RoleBuilderValidation::NotYetValid(not_yet_valid)); + } + Ok(()) + } + + /// Validates `self` by "replaying" the addition of each factor source in `self` to a + /// "simulation" (clone). If the simulation is valid, then `self` is valid. + pub(crate) fn validate(&self) -> RoleBuilderMutateResult { + let mut simulation = Self::new(); + + // Validate override factors + for f in self.get_override_factors() { + let validation = simulation.add_factor_source_to_override(*f); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => continue, + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => { + return validation + } + } + } + + // Validate threshold factors + for f in self.get_threshold_factors() { + let validation = + simulation._add_factor_source_to_list(*f, Threshold); + match validation.as_ref() { + Ok(()) | Err(NotYetValid(_)) => continue, + Err(ForeverInvalid(_)) | Err(BasicViolation(_)) => { + return validation + } + } + } + + // Validate threshold count + if self.role() == RoleKind::Primary { + self.validate_threshold_for_primary()?; + } else if self.get_threshold() != 0 { + match self.role() { + Primary => unreachable!( + "Primary role should have been handled earlier" + ), + Recovery => { + return RoleBuilderMutateResult::basic_violation( + RecoveryCannotSetThreshold, + ) + } + Confirmation => { + return RoleBuilderMutateResult::basic_violation( + ConfirmationCannotSetThreshold, + ) + } + } + } + + if self.threshold_contains_factor_source_of_kind( + FactorSourceKind::Password, + ) { + self.validation_for_addition_of_password_to_primary(Threshold)?; + } + + if self.all_factors().is_empty() { + return RoleBuilderMutateResult::not_yet_valid( + RoleMustHaveAtLeastOneFactor, + ); + } + + Ok(()) + } + + fn validation_for_addition_of_factor_source_of_kind_to_override_for_non_primary_role( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + match self.role() { + RoleKind::Primary => { + unreachable!("Should have branched to 'primary' earlier, this is programmer error.") + } + RoleKind::Confirmation => self + .validation_for_addition_of_factor_source_of_kind_to_override_for_confirmation( + factor_source_kind, + ), + RoleKind::Recovery => self + .validation_for_addition_of_factor_source_of_kind_to_override_for_recovery( + factor_source_kind, + ), + } + } + + /// For each factor source in the given set, return a validation status + /// for adding it to factor list of the given kind (`factor_list_kind`) + pub(crate) fn validation_for_addition_of_factor_source_for_each( + &self, + factor_list_kind: FactorListKind, + factor_sources: &IndexSet, + ) -> IndexSet { + factor_sources + .iter() + .map(|factor_source_id| { + let validation_status = self + .validation_for_addition_of_factor_source_to_list( + factor_source_id, + factor_list_kind, + ); + FactorSourceInRoleBuilderValidationStatus::new( + self.role(), + *factor_source_id, + validation_status, + ) + }) + .collect() + } + + fn validation_for_addition_of_factor_source_to_list( + &self, + factor_source_id: &FactorSourceID, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + if self.contains_factor_source(factor_source_id) { + return RoleBuilderMutateResult::forever_invalid( + FactorSourceAlreadyPresent, + ); + } + let factor_source_kind = factor_source_id.get_factor_source_kind(); + self._validation_add(factor_source_kind, factor_list_kind) + } + + fn contains_factor_source( + &self, + factor_source_id: &FactorSourceID, + ) -> bool { + self.override_contains_factor_source(factor_source_id) + || self.threshold_contains_factor_source(factor_source_id) + } + + fn contains_factor_source_of_kind( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self.override_contains_factor_source_of_kind(factor_source_kind) + || self.threshold_contains_factor_source_of_kind(factor_source_kind) + } + + /// Lowers the threshold if the deleted factor source is in the threshold list + /// and if after removal of `factor_source_id` `self.threshold > self.threshold_factors.len()` + /// + /// Returns `Ok` if `factor_source_id` was found and deleted. However, does not call `self.validate()`, + /// So state might still be invalid, i.e. we return the result of the action of removal, not the + /// state validation status. + pub(crate) fn remove_factor_source( + &mut self, + factor_source_id: &FactorSourceID, + ) -> RoleBuilderMutateResult { + if !self.contains_factor_source(factor_source_id) { + return RoleBuilderMutateResult::basic_violation( + FactorSourceNotFound, + ); + } + + let remove = |xs: &mut Vec| { + let index = xs + .iter() + .position(|f| f == factor_source_id) + .expect("Called remove of non existing FactorSourceID, this is a programmer error, should have checked if it exists before calling remove."); + xs.remove(index); + }; + + if self.override_contains_factor_source(factor_source_id) { + remove(self.mut_override_factors()) + } else if self.threshold_contains_factor_source(factor_source_id) { + // We use `else if` to highlight the fact that a factor cannot + // ever be in both override and threshold list. + remove(self.mut_threshold_factors()); + let threshold_factors_len = + self.get_threshold_factors().len() as u8; + if self.get_threshold() > threshold_factors_len { + // N.B. we don't use `set_threshold` since this might be a + // temporary invalid state, if e.g. primary role does not have + // any factors. + self.unchecked_set_threshold(threshold_factors_len); + } + } + Ok(()) + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_list_for_primary( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + match factor_source_kind { + FactorSourceKind::Password => { + return self.validation_for_addition_of_password_to_primary( + factor_list_kind, + ) + } + FactorSourceKind::SecurityQuestions => { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotContainSecurityQuestions, + ); + } + FactorSourceKind::TrustedContact => { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotContainTrustedContact, + ); + } + FactorSourceKind::Device => { + if self.contains_factor_source_of_kind(FactorSourceKind::Device) + { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotHaveMultipleDevices, + ); + } + } + FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::OffDeviceMnemonic => {} + } + Ok(()) + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_override_for_recovery( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Recovery); + match factor_source_kind { + FactorSourceKind::Device + | FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::OffDeviceMnemonic + | FactorSourceKind::TrustedContact => Ok(()), + FactorSourceKind::SecurityQuestions => { + RoleBuilderMutateResult::forever_invalid( + RecoveryRoleSecurityQuestionsNotSupported, + ) + } + FactorSourceKind::Password => { + RoleBuilderMutateResult::forever_invalid( + RecoveryRolePasswordNotSupported, + ) + } + } + } + + #[cfg(not(tarpaulin_include))] // false negative + fn validation_for_addition_of_factor_source_of_kind_to_override_for_confirmation( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Confirmation); + match factor_source_kind { + FactorSourceKind::Device + | FactorSourceKind::LedgerHQHardwareWallet + | FactorSourceKind::ArculusCard + | FactorSourceKind::Password + | FactorSourceKind::OffDeviceMnemonic + | FactorSourceKind::SecurityQuestions => Ok(()), + FactorSourceKind::TrustedContact => { + RoleBuilderMutateResult::forever_invalid( + ConfirmationRoleTrustedContactNotSupported, + ) + } + } + } +} + +// ======================= +// ======== RULES ======== +// ======================= +impl RoleBuilder { + fn validation_for_addition_of_password_to_primary( + &self, + factor_list_kind: FactorListKind, + ) -> RoleBuilderMutateResult { + assert_eq!(self.role(), RoleKind::Primary); + + if factor_list_kind != Threshold { + return RoleBuilderMutateResult::forever_invalid( + PrimaryCannotHavePasswordInOverrideList, + ); + } + + let factor_source_kind = FactorSourceKind::Password; + + let is_alone = self + .factor_sources_not_of_kind_to_list_of_kind( + factor_source_kind, + Threshold, + ) + .is_empty(); + + if is_alone { + return RoleBuilderMutateResult::not_yet_valid( + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + ); + } + + if self.get_threshold() < 2 { + return RoleBuilderMutateResult::not_yet_valid( + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + ); + } + + Ok(()) + } + + pub(crate) fn factor_sources_not_of_kind_to_list_of_kind( + &self, + factor_source_kind: FactorSourceKind, + factor_list_kind: FactorListKind, + ) -> Vec { + let filter = |xs: &Vec| -> Vec { + xs.iter() + .filter(|f| f.get_factor_source_kind() != factor_source_kind) + .cloned() + .collect() + }; + match factor_list_kind { + Override => filter(self.get_override_factors()), + Threshold => filter(self.get_threshold_factors()), + } + } +} + +#[cfg(test)] +pub(crate) fn test_duplicates_not_allowed( + sut: RoleBuilder, + list: FactorListKind, + factor_source_id: FactorSourceID, +) { + // Arrange + let mut sut = sut; + + sut._add_factor_source_to_list(factor_source_id, list) + .unwrap(); + + // Act + let res = sut._add_factor_source_to_list( + factor_source_id, // oh no, duplicate! + list, + ); + + // Assert + assert!(matches!( + res, + RoleBuilderMutateResult::Err(ForeverInvalid( + ForeverInvalidReason::FactorSourceAlreadyPresent + )) + )); +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn primary_duplicates_not_allowed() { + test_duplicates_not_allowed( + PrimaryRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + test_duplicates_not_allowed( + PrimaryRoleBuilder::new(), + Threshold, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn recovery_duplicates_not_allowed() { + test_duplicates_not_allowed( + RecoveryRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn confirmation_duplicates_not_allowed() { + test_duplicates_not_allowed( + ConfirmationRoleBuilder::new(), + Override, + FactorSourceID::sample_arculus(), + ); + } + + #[test] + fn recovery_cannot_add_factors_to_threshold() { + let mut sut = RecoveryRoleBuilder::new(); + let res = sut._add_factor_source_to_list( + FactorSourceID::sample_ledger(), + Threshold, + ); + assert_eq!( + res, + Err(BasicViolation(BasicViolation::RecoveryCannotSetThreshold)) + ); + } + + #[test] + fn confirmation_cannot_add_factors_to_threshold() { + let mut sut = ConfirmationRoleBuilder::new(); + let res = sut._add_factor_source_to_list( + FactorSourceID::sample_ledger(), + Threshold, + ); + assert_eq!( + res, + Err(BasicViolation( + BasicViolation::ConfirmationCannotSetThreshold + )) + ); + } + + #[test] + fn recovery_validation_add_is_err_for_threshold() { + let sut = RecoveryRoleBuilder::new(); + let res = sut._validation_add(FactorSourceKind::Device, Threshold); + assert_eq!( + res, + RoleBuilderMutateResult::basic_violation( + BasicViolation::threshold_list_not_supported_for_role( + RoleKind::Recovery + ) + ) + ); + } + + #[test] + fn confirmation_validation_add_is_err_for_threshold() { + let sut = ConfirmationRoleBuilder::new(); + let res = sut._validation_add(FactorSourceKind::Device, Threshold); + assert_eq!( + res, + RoleBuilderMutateResult::basic_violation( + BasicViolation::threshold_list_not_supported_for_role( + RoleKind::Confirmation + ) + ) + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder_unit_tests.rs b/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder_unit_tests.rs new file mode 100644 index 000000000..2672bcee6 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/builder/roles_builder_unit_tests.rs @@ -0,0 +1,88 @@ +#![cfg(test)] + +use crate::prelude::*; + +use NotYetValidReason::*; + +type MutRes = RoleBuilderMutateResult; + +#[test] +fn validate_override_for_ever_invalid() { + let sut = PrimaryRoleBuilder::with_factors( + 0, + vec![], + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger(), + ], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::FactorSourceAlreadyPresent + ) + ); +} + +#[test] +fn validate_threshold_for_ever_invalid() { + let sut = PrimaryRoleBuilder::with_factors( + 1, + vec![ + FactorSourceID::sample_ledger(), + FactorSourceID::sample_ledger(), + ], + vec![], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::forever_invalid( + ForeverInvalidReason::FactorSourceAlreadyPresent + ) + ); +} + +#[test] +fn confirmation_validate_basic_violation() { + let sut = ConfirmationRoleBuilder::with_factors( + 1, + vec![], + vec![FactorSourceID::sample_ledger()], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::basic_violation(BasicViolation::ConfirmationCannotSetThreshold) + ); +} + +#[test] +fn recovery_validate_basic_violation() { + let sut = RecoveryRoleBuilder::with_factors( + 1, + vec![], + vec![FactorSourceID::sample_ledger()], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::basic_violation(BasicViolation::RecoveryCannotSetThreshold) + ); +} + +#[test] +fn primary_validate_not_yet_valid_for_threshold_greater_than_threshold_factors() +{ + let sut = PrimaryRoleBuilder::with_factors( + 1, + vec![], + vec![FactorSourceID::sample_ledger()], + ); + let res = sut.validate(); + assert_eq!( + res, + MutRes::not_yet_valid(ThresholdHigherThanThresholdFactorsLen) + ); +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs new file mode 100644 index 000000000..a75b087f2 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/confirmation_role_with_factor_instances.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; + +pub type ConfirmationRoleWithFactorInstances = + RoleWithFactorInstances<{ ROLE_CONFIRMATION }>; + +impl HasSampleValues for ConfirmationRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().confirmation_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().confirmation_role + } +} + +#[cfg(test)] +mod confirmation_tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ConfirmationRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs new file mode 100644 index 000000000..57a7b1e04 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/general_role_with_hierarchical_deterministic_factor_instances.rs @@ -0,0 +1,295 @@ +use crate::prelude::*; + +/// A general depiction of each of the roles in a `MatrixOfFactorInstances`. +/// `SignaturesCollector` can work on any `RoleKind` when dealing with a securified entity. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct GeneralRoleWithHierarchicalDeterministicFactorInstances { + role: RoleKind, + threshold: u8, + threshold_factors: Vec, + override_factors: Vec, +} + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + pub fn get_threshold(&self) -> u8 { + self.threshold + } + + pub fn get_threshold_factors( + &self, + ) -> Vec { + self.threshold_factors.clone() + } + + pub fn get_override_factors( + &self, + ) -> Vec { + self.override_factors.clone() + } + + pub fn with_factors_and_role( + role: RoleKind, + threshold_factors: impl IntoIterator< + Item = HierarchicalDeterministicFactorInstance, + >, + threshold: u8, + override_factors: impl IntoIterator< + Item = HierarchicalDeterministicFactorInstance, + >, + ) -> Result { + let threshold_factors = threshold_factors.into_iter().collect_vec(); + let override_factors = override_factors.into_iter().collect_vec(); + + // validate + let _ = PrimaryRoleWithFactorInstances::with_factors( + threshold, + threshold_factors + .clone() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + override_factors + .clone() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + ); + + Ok(Self { + role, + threshold, + threshold_factors, + override_factors, + }) + } +} + +impl HasRoleKindObjectSafe + for GeneralRoleWithHierarchicalDeterministicFactorInstances +{ + fn get_role_kind(&self) -> RoleKind { + self.role + } +} + +impl TryFrom<(MatrixOfFactorInstances, RoleKind)> + for GeneralRoleWithHierarchicalDeterministicFactorInstances +{ + type Error = CommonError; + + fn try_from( + (matrix, role_kind): (MatrixOfFactorInstances, RoleKind), + ) -> Result { + let threshold_factors: Vec; + let override_factors: Vec; + let threshold: u8; + + match role_kind { + RoleKind::Primary => { + let role = matrix.primary(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + RoleKind::Recovery => { + let role = matrix.recovery(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + RoleKind::Confirmation => { + let role = matrix.confirmation(); + threshold = role.get_threshold(); + threshold_factors = role.get_threshold_factors().clone(); + override_factors = role.get_override_factors().clone(); + } + } + + Self::with_factors_and_role( + role_kind, + threshold_factors + .iter() + .map(|f| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone()) + }) + .collect::, CommonError>>()?, + threshold, + override_factors + .iter() + .map(|f| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone()) + }) + .collect::, CommonError>>()?, + ) + } +} + +impl GeneralRoleWithHierarchicalDeterministicFactorInstances { + pub fn single_override( + role: RoleKind, + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + assert!(factor.is_securified(), "non securified factor"); + Self::with_factors_and_role(role, [], 0, [factor]) + .expect("Zero threshold with zero threshold factors and one override should not fail.") + } + + pub fn single_threshold( + role: RoleKind, + factor: HierarchicalDeterministicFactorInstance, + ) -> Self { + assert!(factor.is_securified(), "non securified factor"); + Self::with_factors_and_role(role, [factor], 1, []).expect( + "Single threshold with one threshold factor should not fail.", + ) + } +} + +impl HasSampleValues + for GeneralRoleWithHierarchicalDeterministicFactorInstances +{ + fn sample() -> Self { + Self::try_from((MatrixOfFactorInstances::sample(), RoleKind::Primary)) + .expect("Sample should not fail") + } + + fn sample_other() -> Self { + Self::try_from(( + MatrixOfFactorInstances::sample_other(), + RoleKind::Recovery, + )) + .expect("Sample should not fail") + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = GeneralRoleWithHierarchicalDeterministicFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + fn matrix() -> MatrixOfFactorInstances { + MatrixOfFactorInstances::sample() + } + + #[test] + fn test_from_primary_role() { + pretty_assertions::assert_eq!( + SUT::try_from( + (matrix(), RoleKind::Primary) + ).unwrap(), + SUT::with_factors_and_role( + RoleKind::Primary, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0), + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0) + ], + 2, + [] + ).unwrap() + ) + } + + #[test] + fn test_single_threshold() { + pretty_assertions::assert_eq!( + SUT::single_threshold(RoleKind::Primary, HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0)), + SUT::with_factors_and_role( + RoleKind::Primary, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_1_securified_at_index(0) + ], + 1, + [] + ).unwrap() + ) + } + + #[test] + fn test_get_role() { + let test = |role: RoleKind| { + let sut = SUT::single_override( + role, + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0) + ); + assert_eq!(sut.get_role_kind(), role); + }; + test(RoleKind::Primary); + test(RoleKind::Confirmation); + test(RoleKind::Recovery); + } + + #[test] + fn test_from_recovery_role() { + let m = matrix(); + let r = m.recovery(); + assert_eq!( + SUT::try_from((matrix(), RoleKind::Recovery)).unwrap(), + SUT::with_factors_and_role( + RoleKind::Recovery, + [], + 0, + r.get_override_factors() + .clone() + .into_iter() + .map(|f: FactorInstance| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) + .unwrap() + }) + .collect_vec(), + ) + .unwrap() + ) + } + + #[test] + fn test_from_confirmation_role() { + let m = matrix(); + let r = m.confirmation(); + assert_eq!( + SUT::try_from((matrix(), RoleKind::Confirmation)).unwrap(), + SUT::with_factors_and_role( + RoleKind::Confirmation, + [], + 0, + r.get_override_factors() + .clone() + .into_iter() + .map(|f: FactorInstance| { + HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) + .unwrap() + }) + .collect_vec(), + ) + .unwrap() + ) + } + + #[test] + fn test_from_matrix_containing_physical_badge() { + let mut matrix = MatrixOfFactorInstances::sample(); + matrix.primary_role = PrimaryRoleWithFactorInstances::with_factors( + 0, + [], + [FactorInstance::sample_other()], + ); + + assert_eq!( + SUT::try_from((matrix, RoleKind::Primary)), + Err(CommonError::BadgeIsNotVirtualHierarchicalDeterministic) + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/mod.rs similarity index 56% rename from crates/sargon/src/profile/mfa/security_structures/factor_instance_level/mod.rs rename to crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/mod.rs index c0ef2eff0..bb3e03464 100644 --- a/crates/sargon/src/profile/mfa/security_structures/factor_instance_level/mod.rs +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/mod.rs @@ -1,11 +1,11 @@ mod confirmation_role_with_factor_instances; -mod matrix_of_factor_instances; +mod general_role_with_hierarchical_deterministic_factor_instances; mod primary_role_with_factor_instances; mod recovery_role_with_factor_instances; -mod security_structure_of_factor_instances; +mod role_with_factor_instances; pub use confirmation_role_with_factor_instances::*; -pub use matrix_of_factor_instances::*; +pub use general_role_with_hierarchical_deterministic_factor_instances::*; pub use primary_role_with_factor_instances::*; pub use recovery_role_with_factor_instances::*; -pub use security_structure_of_factor_instances::*; +pub use role_with_factor_instances::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs new file mode 100644 index 000000000..d2f5fb042 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/primary_role_with_factor_instances.rs @@ -0,0 +1,111 @@ +use crate::prelude::*; + +pub type PrimaryRoleWithFactorInstances = + RoleWithFactorInstances<{ ROLE_PRIMARY }>; + +impl HasSampleValues for PrimaryRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().primary_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().primary_role + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + #[should_panic] + fn primary_role_non_securified_threshold_instances_is_err() { + let _ = SUT::with_factors( + 1, + [ + HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_10_unsecurified_at_index(0).into() + ], + [] + ); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 2, + "thresholdFactors": [ + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + }, + { + "factorSourceID": { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + }, + "badge": { + "discriminator": "virtualSource", + "virtualSource": { + "discriminator": "hierarchicalDeterministicPublicKey", + "hierarchicalDeterministicPublicKey": { + "publicKey": { + "curve": "curve25519", + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" + }, + "derivationPath": { + "scheme": "cap26", + "path": "m/44H/1022H/1H/525H/1460H/0S" + } + } + } + } + } + ], + "overrideFactors": [] + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs new file mode 100644 index 000000000..7956f523f --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/recovery_role_with_factor_instances.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; + +pub type RecoveryRoleWithFactorInstances = + RoleWithFactorInstances<{ ROLE_RECOVERY }>; + +impl HasSampleValues for RecoveryRoleWithFactorInstances { + fn sample() -> Self { + MatrixOfFactorInstances::sample().recovery_role + } + + fn sample_other() -> Self { + MatrixOfFactorInstances::sample_other().recovery_role + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = RecoveryRoleWithFactorInstances; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs new file mode 100644 index 000000000..fb3f872c1 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_instance_level/role_with_factor_instances.rs @@ -0,0 +1,56 @@ +use crate::prelude::*; + +pub(crate) type RoleWithFactorInstances = + AbstractBuiltRoleWithFactor; + +impl RoleWithFactorInstances { + pub(crate) fn fulfilling_role_of_factor_sources_with_factor_instances( + consuming_instances: &IndexMap, + matrix_of_factor_sources: &MatrixOfFactorSources, + ) -> Result { + let role_kind = RoleKind::from_u8(ROLE).unwrap(); + + let role_of_sources = matrix_of_factor_sources.get_role::(); + assert_eq!(role_of_sources.role(), role_kind); + let threshold: u8 = role_of_sources.get_threshold(); + + // Threshold factors + let threshold_factors = + Self::try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + consuming_instances, + role_of_sources.get_threshold_factors(), + )?; + + // Override factors + let override_factors = + Self::try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + consuming_instances, + role_of_sources.get_override_factors(), + )?; + + let role_with_instances = + Self::with_factors(threshold, threshold_factors, override_factors); + + assert_eq!(role_with_instances.role(), role_kind); + Ok(role_with_instances) + } + + fn try_filling_factor_list_of_role_of_factor_sources_with_factor_instances( + instances: &IndexMap, + from: &[FactorSource], + ) -> Result, CommonError> { + from.iter() + .map(|f| { + if let Some(existing) = instances.get(&f.id_from_hash()) { + let hd_instance = existing.first().ok_or( + CommonError::MissingFactorMappingInstancesIntoRole, + )?; + let instance = FactorInstance::from(hd_instance); + Ok(instance) + } else { + Err(CommonError::MissingFactorMappingInstancesIntoRole) + } + }) + .collect::, CommonError>>() + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs new file mode 100644 index 000000000..e62f29505 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/confirmation_role_with_factor_source_ids.rs @@ -0,0 +1,104 @@ +use crate::prelude::*; + +pub type ConfirmationRoleWithFactorSourceIDs = + RoleWithFactorSourceIds<{ ROLE_CONFIRMATION }>; +pub type ConfirmationRoleWithFactorSourceIds = + ConfirmationRoleWithFactorSourceIDs; + +impl HasSampleValues for ConfirmationRoleWithFactorSourceIds { + /// Config MFA 1.1 + fn sample() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_password()) + .unwrap(); + builder.build().unwrap() + } + + /// Config MFA 2.1 + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_device()) + .unwrap(); + builder.build().unwrap() + } +} +impl HasSampleValues for RecoveryRoleWithFactorSourceIds { + /// Config MFA 1.1 + fn sample() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source(FactorSourceID::sample_ledger()) + .unwrap(); + builder.build().unwrap() + } + + /// Config MFA 3.3 + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source(FactorSourceID::sample_ledger_other()) + .unwrap(); + + builder.build().unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ConfirmationRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + + sut.get_threshold_factors().len() + ); + } + + #[test] + fn assert_json() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/mod.rs new file mode 100644 index 000000000..206017bd7 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/mod.rs @@ -0,0 +1,9 @@ +mod confirmation_role_with_factor_source_ids; +mod primary_role_with_factor_source_ids; +mod recovery_role_with_factor_source_ids; +mod roles_with_factor_ids; + +pub use confirmation_role_with_factor_source_ids::*; +pub use primary_role_with_factor_source_ids::*; +pub use recovery_role_with_factor_source_ids::*; +pub use roles_with_factor_ids::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs new file mode 100644 index 000000000..a4b2974f5 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/primary_role_with_factor_source_ids.rs @@ -0,0 +1,107 @@ +use crate::prelude::*; + +pub type PrimaryRoleWithFactorSourceIDs = + RoleWithFactorSourceIds<{ ROLE_PRIMARY }>; +pub type PrimaryRoleWithFactorSourceIds = PrimaryRoleWithFactorSourceIDs; + +impl PrimaryRoleWithFactorSourceIds { + /// Config MFA 1.1 + pub fn sample_primary() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(2).unwrap(); + builder.build().unwrap() + } +} + +impl HasSampleValues for PrimaryRoleWithFactorSourceIds { + fn sample() -> Self { + Self::sample_primary() + } + + fn sample_other() -> Self { + let mut builder = RoleBuilder::new(); + builder + .add_factor_source_to_threshold(FactorSourceID::sample_device()) + .unwrap(); + + builder + .add_factor_source_to_threshold(FactorSourceID::sample_ledger()) + .unwrap(); + builder.set_threshold(1).unwrap(); + builder.build().unwrap() + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample_primary(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + + sut.get_threshold_factors().len() + ); + } + + #[test] + fn get_threshold() { + let sut = SUT::sample_primary(); + assert_eq!(sut.get_threshold(), 2); + } + + #[test] + fn assert_json_sample_primary() { + let sut = SUT::sample_primary(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs new file mode 100644 index 000000000..040e021b0 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/recovery_role_with_factor_source_ids.rs @@ -0,0 +1,66 @@ +use crate::prelude::*; + +pub type RecoveryRoleWithFactorSourceIDs = + RoleWithFactorSourceIds<{ ROLE_RECOVERY }>; +pub type RecoveryRoleWithFactorSourceIds = RecoveryRoleWithFactorSourceIDs; + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = RecoveryRoleWithFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn get_all_factors() { + let sut = SUT::sample(); + let factors = sut.all_factors(); + assert_eq!( + factors.len(), + sut.get_override_factors().len() + + sut.get_threshold_factors().len() + ); + } + + #[test] + fn assert_json() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs new file mode 100644 index 000000000..8ddab0052 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_id_level/roles_with_factor_ids.rs @@ -0,0 +1,4 @@ +use crate::prelude::*; + +pub type RoleWithFactorSourceIds = + AbstractBuiltRoleWithFactor; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/mod.rs new file mode 100644 index 000000000..4343dd10f --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/mod.rs @@ -0,0 +1,3 @@ +mod role_template; + +pub use role_template::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/role_template.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/role_template.rs new file mode 100644 index 000000000..a183eb2dc --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_kind_level/role_template.rs @@ -0,0 +1,105 @@ +use crate::prelude::*; + +/// A "template" FactorSourceID/FactorSource to be used in a RoleTemplate is +/// FactorSourceKind with some placeholder ID, to distinguish between two different +/// FactorSourceIDs of some kind, e.g. `FactorSourceID::sample()` and `FactorSourceID::sample_other()`. +/// but exactly which FactorSourceID values are not yet known, since this is a template. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct FactorSourceTemplate { + /// The kind of FactorSource, e.g. Device, LedgerHQHardwareWallet, Password, etc. + pub kind: FactorSourceKind, + + /// Some placeholder ID to distinguish between two different FactorSourceIDs + /// to be concretely defined later. + pub id: u8, +} + +pub(crate) type RoleTemplate = + AbstractBuiltRoleWithFactor; + +pub type PrimaryRoleTemplate = RoleTemplate<{ ROLE_PRIMARY }>; +pub type RecoveryRoleTemplate = RoleTemplate<{ ROLE_RECOVERY }>; +pub type ConfirmationRoleTemplate = RoleTemplate<{ ROLE_CONFIRMATION }>; + +impl PrimaryRoleTemplate { + pub(crate) fn new( + threshold_factors: impl IntoIterator, + ) -> Self { + let threshold_factors = threshold_factors.into_iter().collect_vec(); + Self::with_factors(threshold_factors.len() as u8, threshold_factors, []) + } +} + +impl RecoveryRoleTemplate { + pub(crate) fn new( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, [], override_factors) + } +} + +impl ConfirmationRoleTemplate { + pub(crate) fn new( + override_factors: impl IntoIterator, + ) -> Self { + Self::with_factors(0, [], override_factors) + } +} + +impl FactorSourceTemplate { + pub fn new(kind: FactorSourceKind, id: u8) -> Self { + Self { kind, id } + } + + pub fn device() -> Self { + Self::new(FactorSourceKind::Device, 0) + } + + fn ledger_id(id: u8) -> Self { + Self::new(FactorSourceKind::LedgerHQHardwareWallet, id) + } + pub fn ledger() -> Self { + Self::ledger_id(0) + } + + pub fn ledger_other() -> Self { + Self::ledger_id(1) + } + + fn password_id(id: u8) -> Self { + Self::new(FactorSourceKind::Password, id) + } + pub fn password() -> Self { + Self::password_id(0) + } + pub fn password_other() -> Self { + Self::password_id(1) + } + + /// Radix Wallet (UI) calls this "Passphrase" + pub fn off_device_mnemonic() -> Self { + Self::new(FactorSourceKind::OffDeviceMnemonic, 0) + } + + fn trusted_contact_id(id: u8) -> Self { + Self::new(FactorSourceKind::TrustedContact, id) + } + + pub fn trusted_contact() -> Self { + Self::trusted_contact_id(0) + } + + pub fn trusted_contact_other() -> Self { + Self::trusted_contact_id(1) + } + + pub fn security_questions() -> Self { + Self::new(FactorSourceKind::SecurityQuestions, 0) + } +} + +impl IsMaybeKeySpaceAware for FactorSourceTemplate { + fn maybe_key_space(&self) -> Option { + None + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/confirmation_role_with_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs similarity index 54% rename from crates/sargon/src/profile/mfa/security_structures/factor_source_level/confirmation_role_with_factor_sources.rs rename to crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs index 433e99299..56c8e4665 100644 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/confirmation_role_with_factor_sources.rs +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/confirmation_role_with_factor_sources.rs @@ -1,28 +1,19 @@ use crate::prelude::*; +pub type ConfirmationRoleWithFactorSources = + RoleWithFactorSources<{ ROLE_CONFIRMATION }>; + impl HasSampleValues for ConfirmationRoleWithFactorSources { fn sample() -> Self { - Self::new( - [], - 0, - [ - FactorSource::sample_security_questions(), - FactorSource::sample_ledger(), - ], - ) - .unwrap() + let ids = ConfirmationRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() } fn sample_other() -> Self { - Self::new( - [], - 0, - [ - FactorSource::sample_security_questions_other(), - FactorSource::sample_ledger_other(), - ], - ) - .unwrap() + let ids = ConfirmationRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() } } diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/mod.rs similarity index 55% rename from crates/sargon/src/profile/mfa/security_structures/factor_source_level/mod.rs rename to crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/mod.rs index ddaa2484a..b629e2284 100644 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/mod.rs +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/mod.rs @@ -1,12 +1,9 @@ mod confirmation_role_with_factor_sources; -mod matrix_of_factor_sources; mod primary_role_with_factor_sources; mod recovery_role_with_factor_sources; -mod security_structure_of_factor_sources; -mod security_structures_of_factor_sources; +mod roles_with_factor_sources; pub use confirmation_role_with_factor_sources::*; pub use primary_role_with_factor_sources::*; pub use recovery_role_with_factor_sources::*; -pub use security_structure_of_factor_sources::*; -pub use security_structures_of_factor_sources::*; +pub use roles_with_factor_sources::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs new file mode 100644 index 000000000..e7fe56eca --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/primary_role_with_factor_sources.rs @@ -0,0 +1,36 @@ +use crate::prelude::*; + +pub type PrimaryRoleWithFactorSources = RoleWithFactorSources<{ ROLE_PRIMARY }>; + +impl HasSampleValues for PrimaryRoleWithFactorSources { + fn sample() -> Self { + let ids = PrimaryRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } + + fn sample_other() -> Self { + let ids = PrimaryRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = PrimaryRoleWithFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/recovery_role_with_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs similarity index 54% rename from crates/sargon/src/profile/mfa/security_structures/factor_source_level/recovery_role_with_factor_sources.rs rename to crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs index 4a5aaa919..75949db79 100644 --- a/crates/sargon/src/profile/mfa/security_structures/factor_source_level/recovery_role_with_factor_sources.rs +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/recovery_role_with_factor_sources.rs @@ -1,24 +1,19 @@ use crate::prelude::*; +pub type RecoveryRoleWithFactorSources = + RoleWithFactorSources<{ ROLE_RECOVERY }>; + impl HasSampleValues for RecoveryRoleWithFactorSources { fn sample() -> Self { - Self::new( - [ - FactorSource::sample_arculus(), - FactorSource::sample_arculus_other(), - ], - 2, - [FactorSource::sample_ledger()], - ) - .unwrap() + let ids = RecoveryRoleWithFactorSourceIds::sample(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() } + fn sample_other() -> Self { - Self::new( - [FactorSource::sample_arculus_other()], - 1, - [FactorSource::sample_ledger_other()], - ) - .unwrap() + let ids = RecoveryRoleWithFactorSourceIds::sample_other(); + let factor_sources = FactorSources::sample_values_all(); + Self::new(ids, &factor_sources).unwrap() } } diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs new file mode 100644 index 000000000..92810fa69 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/factor_source_level/roles_with_factor_sources.rs @@ -0,0 +1,67 @@ +use crate::prelude::*; + +pub(crate) type RoleWithFactorSources = + AbstractBuiltRoleWithFactor; + +impl RoleWithFactorSources { + fn from( + other: &RoleWithFactorSources, + ) -> Self { + Self::with_factors( + other.get_threshold(), + other.get_threshold_factors().clone(), + other.get_override_factors().clone(), + ) + } +} + +impl MatrixOfFactorSources { + pub(crate) fn get_role( + &self, + ) -> RoleWithFactorSources { + match ROLE { + ROLE_PRIMARY => { + RoleWithFactorSources::::from(&self.primary_role) + } + ROLE_RECOVERY => { + RoleWithFactorSources::::from(&self.recovery_role) + } + ROLE_CONFIRMATION => { + RoleWithFactorSources::::from(&self.confirmation_role) + } + _ => panic!("unknown"), + } + } +} + +impl RoleWithFactorSources { + pub fn new( + role_with_factor_source_ids: RoleWithFactorSourceIds, + factor_sources: &FactorSources, + ) -> Result { + let lookup_f = + |id: &FactorSourceID| -> Result { + factor_sources + .get_id(id) + .ok_or(CommonError::FactorSourceDiscrepancy) + .cloned() + }; + + let lookup = |ids: &Vec| -> Result, CommonError> { + ids.iter() + .map(lookup_f) + .collect::, CommonError>>() + }; + + let threshold_factors = + lookup(role_with_factor_source_ids.get_threshold_factors())?; + let override_factors = + lookup(role_with_factor_source_ids.get_override_factors())?; + + Ok(Self::with_factors( + role_with_factor_source_ids.get_threshold(), + threshold_factors, + override_factors, + )) + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/mod.rs new file mode 100644 index 000000000..818275a84 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/factor_levels/mod.rs @@ -0,0 +1,9 @@ +mod factor_instance_level; +mod factor_source_id_level; +mod factor_source_kind_level; +mod factor_source_level; + +pub use factor_instance_level::*; +pub use factor_source_id_level::*; +pub use factor_source_kind_level::*; +pub use factor_source_level::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/roles/mod.rs b/crates/sargon/src/profile/mfa/security_structures/roles/mod.rs new file mode 100644 index 000000000..dcd18772f --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/roles/mod.rs @@ -0,0 +1,7 @@ +mod abstract_role_builder_or_built; +mod builder; +mod factor_levels; + +pub use abstract_role_builder_or_built::*; +pub use builder::*; +pub use factor_levels::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/security_shield_builder.rs b/crates/sargon/src/profile/mfa/security_structures/security_shield_builder.rs new file mode 100644 index 000000000..db79bed7e --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_shield_builder.rs @@ -0,0 +1,985 @@ +use crate::prelude::*; + +#[derive(Debug)] +pub struct SecurityShieldBuilder { + matrix_builder: RwLock, + name: RwLock, + // We eagerly set this, and we use it inside the `build` method, ensuring + // that for the same *state* of `MatrixBuilder` we always have the same shield! + shield_id: SecurityStructureID, + // We eagerly set this, and we use it inside the `build` method, ensuring + // that for the same *state* of `MatrixBuilder` we always have the same shield! + created_on: Timestamp, +} + +impl Default for SecurityShieldBuilder { + fn default() -> Self { + Self::new() + } +} + +impl SecurityShieldBuilder { + pub fn new() -> Self { + let matrix_builder = MatrixBuilder::new(); + let name = RwLock::new("My Shield".to_owned()); + Self { + matrix_builder: RwLock::new(matrix_builder), + name, + shield_id: SecurityStructureID::from(id()), + created_on: now(), + } + } +} + +impl SecurityShieldBuilder { + fn get(&self, access: impl Fn(&MatrixBuilder) -> R) -> R { + let binding = self.matrix_builder.read().unwrap(); + access(&binding) + } + + // Ignores error and returns a ref to self + fn set(&self, mut write: impl FnMut(&mut MatrixBuilder) -> R) -> &Self { + let mut binding = self.matrix_builder.write().expect("No poison"); + write(&mut binding); + self + } + + fn validation_for_addition_of_factor_source_by_calling( + &self, + factor_sources: Vec, + call: impl Fn( + &MatrixBuilder, + &IndexSet, + ) + -> IndexSet, + ) -> Vec { + let input = &factor_sources.into_iter().collect::>(); + self.get(|builder| call(builder, input)) + .into_iter() + .collect_vec() + } +} + +impl SecurityShieldBuilder { + fn get_factors( + &self, + access: impl Fn(&MatrixBuilder) -> &Vec, + ) -> Vec { + self.get(|builder| { + let factors = access(builder); + factors.to_vec() + }) + } +} + +// ==================== +// ==== GET / READ ==== +// ==================== +impl SecurityShieldBuilder { + pub fn get_threshold(&self) -> u8 { + self.get(|builder| builder.get_threshold()) + } + + pub fn get_number_of_days_until_auto_confirm(&self) -> u16 { + self.get(|builder| builder.get_number_of_days_until_auto_confirm()) + } + + pub fn get_name(&self) -> String { + self.name.read().unwrap().clone() + } + + pub fn get_primary_threshold_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_primary_threshold_factors()) + } + + pub fn get_primary_override_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_primary_override_factors()) + } + + pub fn get_recovery_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_recovery_factors()) + } + + pub fn get_confirmation_factors(&self) -> Vec { + self.get_factors(|builder| builder.get_confirmation_factors()) + } +} + +// ==================== +// ===== MUTATION ===== +// ==================== +impl SecurityShieldBuilder { + pub fn set_name(&self, name: impl AsRef) -> &Self { + *self.name.write().unwrap() = name.as_ref().to_owned(); + self + } + + /// Adds the factor source to the primary role threshold list. + /// + /// Also sets the threshold to 1 this is the first factor set and if + /// the threshold was 0. + pub fn add_factor_source_to_primary_threshold( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + let res = builder + .add_factor_source_to_primary_threshold(factor_source_id); + debug!( + "Add FactorSource to PrimaryRole (threshold) result: {:?}", + res + ); + }) + } + + pub fn add_factor_source_to_primary_override( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + let res = + builder.add_factor_source_to_primary_override(factor_source_id); + debug!( + "Add FactorSource to PrimaryRole (override) result: {:?}", + res + ); + }) + } + + /// Removes the factor from all relevant roles + pub fn remove_factor_from_all_roles( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + builder.remove_factor_from_all_roles(&factor_source_id) + }) + } + + /// Removes factor **only** from the primary role. + pub fn remove_factor_from_primary( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + builder.remove_factor_from_primary(&factor_source_id) + }) + } + + /// Removes factor **only** from the recovery role. + pub fn remove_factor_from_recovery( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + builder.remove_factor_from_recovery(&factor_source_id) + }) + } + + /// Removes factor **only** from the confirmation role. + pub fn remove_factor_from_confirmation( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + builder.remove_factor_from_confirmation(&factor_source_id) + }) + } + + pub fn set_threshold(&self, threshold: u8) -> &Self { + self.set(|builder| builder.set_threshold(threshold)) + } + + pub fn set_number_of_days_until_auto_confirm( + &self, + number_of_days: u16, + ) -> &Self { + self.set(|builder| { + builder.set_number_of_days_until_auto_confirm(number_of_days) + }) + } + + pub fn add_factor_source_to_recovery_override( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + let res = builder + .add_factor_source_to_recovery_override(factor_source_id); + debug!("Add FactorSource to RecoveryRole result: {:?}", res); + }) + } + + pub fn add_factor_source_to_confirmation_override( + &self, + factor_source_id: FactorSourceID, + ) -> &Self { + self.set(|builder| { + let res = builder + .add_factor_source_to_confirmation_override(factor_source_id); + debug!("Add FactorSource to ConfirmationRole result: {:?}", res); + }) + } + + pub fn reset_recovery_and_confirmation_role_state(&self) -> &Self { + self.set(|builder| { + builder.reset_recovery_and_confirmation_role_state(); + }) + } +} + +impl SecurityShieldBuilder { + fn _validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.get(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_confirmation_override( + factor_source_kind + ) + }) + } + + fn _validation_for_addition_of_factor_source_of_kind_to_recovery_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.get(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_recovery_override( + factor_source_kind + ) + }) + } + + fn _validation_for_addition_of_factor_source_of_kind_to_primary_override( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.get(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_primary_override( + factor_source_kind + ) + }) + } + + fn _validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + &self, + factor_source_kind: FactorSourceKind, + ) -> RoleBuilderMutateResult { + self.get(|builder| { + builder.validation_for_addition_of_factor_source_of_kind_to_primary_threshold( + factor_source_kind + ) + }) + } +} + +impl SecurityShieldBuilder { + /// Returns `true` for `Ok` and `Err(NotYetValid)`. + pub fn addition_of_factor_source_of_kind_to_primary_threshold_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_primary_threshold(factor_source_kind).is_valid_or_can_be() + } + + /// Returns `true` for `Ok` and `Err(NotYetValid)`. + pub fn addition_of_factor_source_of_kind_to_primary_override_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_primary_override(factor_source_kind).is_valid_or_can_be() + } + + /// Returns `true` for `Ok` and `Err(NotYetValid)`. + pub fn addition_of_factor_source_of_kind_to_recovery_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_recovery_override(factor_source_kind).is_valid_or_can_be() + } + + /// Returns `true` for `Ok` and `Err(NotYetValid)`. + pub fn addition_of_factor_source_of_kind_to_confirmation_is_valid_or_can_be( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_confirmation_override(factor_source_kind).is_valid_or_can_be() + } +} + +pub trait IsValidOrCanBecomeValid { + fn is_valid_or_can_be(&self) -> bool; +} +impl IsValidOrCanBecomeValid for Result { + fn is_valid_or_can_be(&self) -> bool { + match self { + Ok(_) => true, + Err(RoleBuilderValidation::BasicViolation(_)) + | Err(RoleBuilderValidation::ForeverInvalid(_)) => false, + Err(RoleBuilderValidation::NotYetValid(_)) => true, + } + } +} + +impl SecurityShieldBuilder { + pub fn addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_primary_threshold(factor_source_kind) + .is_ok() + } + + pub fn addition_of_factor_source_of_kind_to_primary_override_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_primary_override(factor_source_kind) + .is_ok() + } + + pub fn addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_recovery_override(factor_source_kind) + .is_ok() + } + + pub fn addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + &self, + factor_source_kind: FactorSourceKind, + ) -> bool { + self._validation_for_addition_of_factor_source_of_kind_to_confirmation_override(factor_source_kind) + .is_ok() + } +} + +impl SecurityShieldBuilder { + pub fn validation_for_addition_of_factor_source_to_primary_threshold_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_primary_threshold_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_primary_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_primary_override_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_recovery_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder + .validation_for_addition_of_factor_source_to_recovery_override_for_each(input) + }, + ) + } + + pub fn validation_for_addition_of_factor_source_to_confirmation_override_for_each( + &self, + factor_sources: Vec, + ) -> Vec { + self.validation_for_addition_of_factor_source_by_calling( + factor_sources, + |builder, input| { + builder.validation_for_addition_of_factor_source_to_confirmation_override_for_each( + input, + ) + }, + ) + } +} + +impl SecurityShieldBuilder { + /// `None` means valid! + pub fn validate(&self) -> Option { + if DisplayName::new(self.get_name()).is_err() { + return Some(SecurityShieldBuilderInvalidReason::ShieldNameInvalid); + } + self.get(|builder| { + let r = builder.validate(); + r.as_shield_validation() + }) + } + + pub fn build( + &self, + ) -> Result< + SecurityStructureOfFactorSourceIds, + SecurityShieldBuilderInvalidReason, + > { + let matrix_result = self.get(|builder| builder.build()); + + if let Some(validation_error) = matrix_result.as_shield_validation() { + return Err(validation_error); + }; + assert!( + matrix_result.is_ok(), + "Programmer error, bad implementation of `into_validation`" + ); + let matrix_of_factors = matrix_result.unwrap(); + + let name = self.get_name(); + let display_name = DisplayName::new(name).map_err(|e| { + error!("Invalid DisplayName {:?}", e); + SecurityShieldBuilderInvalidReason::ShieldNameInvalid + })?; + + let metadata = SecurityStructureMetadata::with_details( + self.shield_id, + display_name, + self.created_on, + self.created_on, + ); + + let shield = SecurityStructureOfFactorSourceIds { + matrix_of_factors, + metadata, + }; + Ok(shield) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityShieldBuilder; + + #[test] + fn add_factor_to_primary_threshold_does_not_change_already_set_threshold() { + let sut = SUT::new(); + sut.set_threshold(42); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ); + assert_eq!(sut.get_threshold(), 42); + } + + #[test] + fn test() { + let sut = SUT::default(); + + let _ = sut + .set_name("S.H.I.E.L.D.") + // Primary + .set_number_of_days_until_auto_confirm(42) + .add_factor_source_to_primary_threshold( + // also sets threshold -> 1 + FactorSourceID::sample_device(), + ) + .add_factor_source_to_primary_override( + FactorSourceID::sample_arculus(), + ) + .add_factor_source_to_primary_override( + FactorSourceID::sample_arculus_other(), + ) + // Recovery + .add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ) + .add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger_other(), + ) + // Confirmation + .add_factor_source_to_confirmation_override( + FactorSourceID::sample_device(), + ) + .remove_factor_from_primary(FactorSourceID::sample_arculus_other()) + .remove_factor_from_recovery(FactorSourceID::sample_ledger_other()); + + let shield0 = sut.build().unwrap(); + let shield = sut.build().unwrap(); + pretty_assertions::assert_eq!(shield0, shield); + + assert_eq!(shield.metadata.display_name.value, "S.H.I.E.L.D."); + assert_eq!( + shield.matrix_of_factors.primary().get_override_factors(), + &vec![FactorSourceID::sample_arculus()] + ); + assert_eq!(shield.matrix_of_factors.primary().get_threshold(), 1); + assert_eq!( + shield.matrix_of_factors.recovery().get_override_factors(), + &vec![FactorSourceID::sample_ledger()] + ); + assert_eq!( + shield + .matrix_of_factors + .confirmation() + .get_override_factors(), + &vec![FactorSourceID::sample_device()] + ); + } + + fn test_addition_of_factor_source_of_kind_to_primary( + list_kind: FactorListKind, + is_fully_valid: impl Fn(&SUT, FactorSourceKind) -> bool, + can_be: impl Fn(&SUT, FactorSourceKind) -> bool, + add: impl Fn(&SUT, FactorSourceID) -> &SUT, + ) { + let sut_owned = SUT::new(); + let sut = &sut_owned; + assert!(can_be(sut, FactorSourceKind::Device)); + + if list_kind == FactorListKind::Threshold { + assert!(!is_fully_valid(sut, FactorSourceKind::Password)); // never alone + assert!(can_be(sut, FactorSourceKind::Password)); // lenient + + // now lets adding a Device => subsequent calls to `is_fully_valid` will return false + add(sut, FactorSourceID::sample_device()); + add(sut, FactorSourceID::sample_ledger()); + + sut.set_threshold(2); + assert!(is_fully_valid(sut, FactorSourceKind::Password)); // not alone any more! + assert!(can_be(sut, FactorSourceKind::Password)); + } else { + // now lets adding a Device => subsequent calls to `is_fully_valid` will return false + add(sut, FactorSourceID::sample_device()); + } + + assert!(!is_fully_valid(sut, FactorSourceKind::Device)); + + // TODO: Unsure about this, we do not count current state and query "can I add (another) Device?" as something + // which can become valid. It would require deletion of current Device factor. Maybe we should change this? + // Not sure... lets keep it as is for now! And lets see how UI integration "feels". + assert!(!can_be(sut, FactorSourceKind::Device)); + + // make it valid again + sut.remove_factor_from_all_roles(FactorSourceID::sample_device()); + + assert!(is_fully_valid(sut, FactorSourceKind::Device)); + assert!(can_be(sut, FactorSourceKind::Device)); + } + + #[test] + fn test_addition_of_factor_source_of_kind_to_primary_threshold() { + test_addition_of_factor_source_of_kind_to_primary( + FactorListKind::Threshold, + SUT::addition_of_factor_source_of_kind_to_primary_threshold_is_fully_valid, + SUT::addition_of_factor_source_of_kind_to_primary_threshold_is_valid_or_can_be, + SUT::add_factor_source_to_primary_threshold, + ); + } + + #[test] + fn test_addition_of_factor_source_of_kind_to_primary_override() { + test_addition_of_factor_source_of_kind_to_primary( + FactorListKind::Override, + SUT::addition_of_factor_source_of_kind_to_primary_override_is_fully_valid, + SUT::addition_of_factor_source_of_kind_to_primary_override_is_valid_or_can_be, + SUT::add_factor_source_to_primary_override, + ); + } + + #[test] + fn test_addition_of_factor_source_of_kind_to_recovery_is_fully_valid() { + let sut = SUT::new(); + + let result = sut + .addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + FactorSourceKind::Device, + ); + assert!(result); + + let result = sut + .addition_of_factor_source_of_kind_to_recovery_is_fully_valid( + FactorSourceKind::Password, + ); + assert!(!result); + } + + #[test] + fn test_addition_of_factor_source_of_kind_to_confirmation_is_fully_valid() { + let sut = SUT::new(); + + let result = sut + .addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + FactorSourceKind::Device, + ); + assert!(result); + + let result = sut + .addition_of_factor_source_of_kind_to_confirmation_is_fully_valid( + FactorSourceKind::TrustedContact, + ); + assert!(!result); + } + + #[test] + fn test_validation_for_addition_of_factor_source_to_primary_threshold_for_each( + ) { + let sut = SUT::new(); + + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ); + + let xs = sut.validation_for_addition_of_factor_source_to_primary_threshold_for_each( + vec![ + FactorSourceID::sample_device(), + FactorSourceID::sample_device_other(), + ], + ); + + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + vec![ + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device(), + ForeverInvalidReason::FactorSourceAlreadyPresent + ), + FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Primary, + FactorSourceID::sample_device_other(), + ForeverInvalidReason::PrimaryCannotHaveMultipleDevices + ), + ] + ); + } + + #[test] + fn test_validation_for_addition_of_factor_source_to_recovery_override_for_each( + ) { + let sut = SUT::new(); + + let xs = sut.validation_for_addition_of_factor_source_to_recovery_override_for_each( + vec![ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ], + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_password(), + FactorSourceID::sample_password_other(), + FactorSourceID::sample_security_questions(), + FactorSourceID::sample_security_questions_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Recovery, + fsid, + if fsid.get_factor_source_kind() == FactorSourceKind::SecurityQuestions { + ForeverInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + } else { + ForeverInvalidReason::RecoveryRolePasswordNotSupported + } + ) + ) + .collect::>() + ); + } + + #[test] + fn test_validation_for_addition_of_factor_source_to_confirmation_override_for_each( + ) { + let sut = SUT::new(); + let xs = sut + .validation_for_addition_of_factor_source_to_confirmation_override_for_each( + vec![ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ], + ); + pretty_assertions::assert_eq!( + xs.into_iter().collect::>(), + [ + FactorSourceID::sample_trusted_contact(), + FactorSourceID::sample_trusted_contact_other(), + ] + .into_iter() + .map( + |fsid| FactorSourceInRoleBuilderValidationStatus::forever_invalid( + RoleKind::Confirmation, + fsid, + ForeverInvalidReason::ConfirmationRoleTrustedContactNotSupported + ) + ) + .collect::>() + ); + } +} + +#[cfg(test)] +mod test_invalid { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityShieldBuilder; + + #[test] + fn primary_role_must_have_at_least_one_factor() { + let sut = SUT::new(); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::PrimaryRoleMustHaveAtLeastOneFactor + ); + } + + #[test] + fn primary_role_with_threshold_cannot_be_zero_with_factors() { + let sut = SUT::new(); + sut.add_factor_source_to_primary_threshold( + // bumped threshold + FactorSourceID::sample_device(), + ); + assert_eq!(sut.get_threshold(), 1); + sut.set_threshold(0); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero + ); + } + + #[test] + fn recovery_role_must_have_at_least_one_factor() { + let sut = SUT::new(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::RecoveryRoleMustHaveAtLeastOneFactor + ); + } + + #[test] + fn confirmation_role_must_have_at_least_one_factor() { + let sut = SUT::new(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::ConfirmationRoleMustHaveAtLeastOneFactor + ); + } + + #[test] + fn valid_is_none() { + let sut = SUT::new(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + assert!(sut.validate().is_none()); + } + + fn valid() -> SUT { + let sut = SUT::new(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ); + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + sut + } + + #[test] + fn shield_name_invalid_empty() { + let sut = valid(); + sut.set_name(""); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::ShieldNameInvalid + ); + } + + #[test] + fn shield_name_truncated_if_too_long() { + let sut = valid(); + sut.set_name( + "This shield name's too long and it is going to get truncated", + ); + let shield = sut.build().unwrap(); + assert_eq!( + shield.metadata.display_name.value, + "This shield name's too long an" + ); + } + + #[test] + fn number_of_auto_confirm_days_invalid() { + let sut = valid(); + sut.set_number_of_days_until_auto_confirm(0); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero + ); + } + + #[test] + fn recovery_and_confirmation_factors_overlap() { + let sut = SUT::new(); + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_device(), + ); + + let same = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_recovery_override(same); + sut.add_factor_source_to_confirmation_override( + same, // same factor! not allowed + ); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::RecoveryAndConfirmationFactorsOverlap + ); + } + + #[test] + fn single_factor_used_in_primary_must_not_be_used_in_any_other_role_in_recovery( + ) { + let sut = SUT::new(); + let same = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(same); + + sut.add_factor_source_to_recovery_override( + same, // same factor! not allowed + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole + ); + } + + #[test] + fn single_factor_used_in_primary_must_not_be_used_in_any_other_role_in_confirmation( + ) { + let sut = SUT::new(); + let same = FactorSourceID::sample_ledger(); + sut.add_factor_source_to_primary_override(same); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_arculus(), + ); + sut.add_factor_source_to_confirmation_override( + same, // same factor! not allowed + ); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole + ); + } + + #[test] + fn primary_role_with_password_in_threshold_list_must_threshold_greater_than_one( + ) { + let sut = SUT::new(); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + + sut.set_threshold(1); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_password(), + ); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_device(), + ); + + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne + ); + } + + #[test] + fn primary_role_with_password_in_threshold_list_must_have_another_factor() { + let sut = SUT::new(); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + + sut.set_threshold(1); + sut.add_factor_source_to_primary_threshold( + FactorSourceID::sample_password(), + ); + + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + ); + } + + #[test] + fn primary_role_with_password_in_override_does_not_get_added() { + let sut = SUT::new(); + + sut.add_factor_source_to_recovery_override( + FactorSourceID::sample_ledger(), + ); + sut.add_factor_source_to_confirmation_override( + FactorSourceID::sample_arculus(), + ); + + sut.add_factor_source_to_primary_override( + FactorSourceID::sample_password(), + ); + assert!(sut.get_primary_override_factors().is_empty()); // did not get added + + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::PrimaryRoleMustHaveAtLeastOneFactor + ); + } + + #[test] + fn template() { + // use this to create more tests... + let sut = valid(); + sut.set_name(""); + assert_eq!( + sut.validate().unwrap(), + SecurityShieldBuilderInvalidReason::ShieldNameInvalid + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs b/crates/sargon/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs new file mode 100644 index 000000000..728f3ae0a --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_shield_builder_invalid_reason.rs @@ -0,0 +1,248 @@ +use crate::prelude::*; + +pub trait AsShieldBuilderViolation { + fn as_shield_validation( + &self, + ) -> Option; +} + +impl AsShieldBuilderViolation + for Result +{ + fn as_shield_validation( + &self, + ) -> Option { + match self { + Result::Err(err) => err.as_shield_validation(), + Result::Ok(_) => None, + } + } +} +impl AsShieldBuilderViolation for MatrixBuilderValidation { + fn as_shield_validation( + &self, + ) -> Option { + match self { + MatrixBuilderValidation::RoleInIsolation { role, violation } => { + (*role, *violation).as_shield_validation() + } + MatrixBuilderValidation::CombinationViolation(violation) => { + violation.as_shield_validation() + } + } + } +} + +impl AsShieldBuilderViolation for MatrixRolesInCombinationViolation { + fn as_shield_validation( + &self, + ) -> Option { + match self { + Self::Basic(val) => val.as_shield_validation(), + Self::ForeverInvalid(val) => val.as_shield_validation(), + Self::NotYetValid(val) => val.as_shield_validation(), + } + } +} + +impl AsShieldBuilderViolation for MatrixRolesInCombinationBasicViolation { + fn as_shield_validation( + &self, + ) -> Option { + use MatrixRolesInCombinationBasicViolation::*; + match self { + FactorSourceNotFoundInAnyRole => + unreachable!("Cannot happen since this error is not returned by 'validate'/'build'."), + NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero => { + Some(SecurityShieldBuilderInvalidReason::NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero) + } + } + } +} +impl AsShieldBuilderViolation for MatrixRolesInCombinationForeverInvalid { + fn as_shield_validation( + &self, + ) -> Option { + use MatrixRolesInCombinationForeverInvalid::*; + match self { + RecoveryAndConfirmationFactorsOverlap => { + Some(SecurityShieldBuilderInvalidReason::RecoveryAndConfirmationFactorsOverlap) + } + } + } +} +impl AsShieldBuilderViolation for MatrixRolesInCombinationNotYetValid { + fn as_shield_validation( + &self, + ) -> Option { + use MatrixRolesInCombinationNotYetValid::*; + + match self { + SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole => { + Some(SecurityShieldBuilderInvalidReason::SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole) + } + } + } +} + +impl AsShieldBuilderViolation for (RoleKind, RoleBuilderValidation) { + fn as_shield_validation( + &self, + ) -> Option { + let (role_kind, violation) = self; + match violation { + RoleBuilderValidation::BasicViolation(basic) => unreachable!("Programmer error. Should have prevented this from happening: '{:?}'", basic), + RoleBuilderValidation::ForeverInvalid(forever) => { + forever.as_shield_validation() + } + RoleBuilderValidation::NotYetValid(not_yet_valid) => { + (*role_kind, *not_yet_valid).as_shield_validation() + } + } + } +} + +impl AsShieldBuilderViolation for ForeverInvalidReason { + fn as_shield_validation( + &self, + ) -> Option { + use ForeverInvalidReason::*; + let reason = match self { + FactorSourceAlreadyPresent => SecurityShieldBuilderInvalidReason::FactorSourceAlreadyPresent, + PrimaryCannotHaveMultipleDevices => { + SecurityShieldBuilderInvalidReason::PrimaryCannotHaveMultipleDevices + } + PrimaryCannotHavePasswordInOverrideList => { + SecurityShieldBuilderInvalidReason::PrimaryCannotHavePasswordInOverrideList + } + PrimaryCannotContainSecurityQuestions => { + SecurityShieldBuilderInvalidReason::PrimaryCannotContainSecurityQuestions + } + PrimaryCannotContainTrustedContact => { + SecurityShieldBuilderInvalidReason::PrimaryCannotContainTrustedContact + } + RecoveryRoleSecurityQuestionsNotSupported => { + SecurityShieldBuilderInvalidReason::RecoveryRoleSecurityQuestionsNotSupported + } + RecoveryRolePasswordNotSupported => { + SecurityShieldBuilderInvalidReason::RecoveryRolePasswordNotSupported + } + ConfirmationRoleTrustedContactNotSupported => { + SecurityShieldBuilderInvalidReason::ConfirmationRoleTrustedContactNotSupported + } + }; + Some(reason) + } +} + +impl SecurityShieldBuilderInvalidReason { + pub(crate) fn role_must_have_at_least_one_factor( + role_kind: &RoleKind, + ) -> Self { + match role_kind { + RoleKind::Primary => Self::PrimaryRoleMustHaveAtLeastOneFactor, + RoleKind::Recovery => Self::RecoveryRoleMustHaveAtLeastOneFactor, + RoleKind::Confirmation => { + Self::ConfirmationRoleMustHaveAtLeastOneFactor + } + } + } +} + +impl AsShieldBuilderViolation for (RoleKind, NotYetValidReason) { + fn as_shield_validation( + &self, + ) -> Option { + let (role_kind, violation) = self; + use NotYetValidReason::*; + let reason = match violation { + RoleMustHaveAtLeastOneFactor => SecurityShieldBuilderInvalidReason::role_must_have_at_least_one_factor(role_kind), + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor => { + SecurityShieldBuilderInvalidReason::PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor + } + PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero => { + SecurityShieldBuilderInvalidReason::PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero + } + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne => { + SecurityShieldBuilderInvalidReason::PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne + } + ThresholdHigherThanThresholdFactorsLen => { + SecurityShieldBuilderInvalidReason::ThresholdHigherThanThresholdFactorsLen + } + }; + Some(reason) + } +} + +// #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] + +#[repr(u32)] +#[derive(Clone, Debug, thiserror::Error, PartialEq)] +pub enum SecurityShieldBuilderInvalidReason { + #[error("Shield name is invalid")] + ShieldNameInvalid, + + #[error("The number of days until auto confirm must be greater than zero")] + NumberOfDaysUntilAutoConfirmMustBeGreaterThanZero, + + #[error("Recovery and confirmation factors overlap. No factor may be used in both the recovery and confirmation roles")] + RecoveryAndConfirmationFactorsOverlap, + + #[error("The single factor used in the primary role must not be used in any other role")] + SingleFactorUsedInPrimaryMustNotBeUsedInAnyOtherRole, + + // ========================= + // NotYetValidReason + // ========================= + #[error("PrimaryRole must have at least one factor")] + PrimaryRoleMustHaveAtLeastOneFactor, + + #[error("RecoveryRole must have at least one factor")] + RecoveryRoleMustHaveAtLeastOneFactor, + + #[error("ConfirmationRole must have at least one factor")] + ConfirmationRoleMustHaveAtLeastOneFactor, + + #[error( + "Primary role with password in threshold list must have another factor" + )] + PrimaryRoleWithPasswordInThresholdListMustHaveAnotherFactor, + + #[error( + "Primary role with threshold factors cannot have a threshold of zero" + )] + PrimaryRoleWithThresholdFactorsCannotHaveAThresholdValueOfZero, + + #[error("Primary role with password in threshold list must have threshold greater than one")] + PrimaryRoleWithPasswordInThresholdListMustThresholdGreaterThanOne, + + #[error("Threshold higher than threshold factors len")] + ThresholdHigherThanThresholdFactorsLen, + + // ================================ + // ForeverInvalidReason + // ================================ + #[error("Factor source already present")] + FactorSourceAlreadyPresent, + + #[error("Primary role cannot have multiple devices")] + PrimaryCannotHaveMultipleDevices, + + #[error("Primary role cannot have password in override list")] + PrimaryCannotHavePasswordInOverrideList, + + #[error("Primary role cannot contain Security Questions")] + PrimaryCannotContainSecurityQuestions, + + #[error("Primary role cannot contain Trusted Contact")] + PrimaryCannotContainTrustedContact, + + #[error("Recovery role Security Questions not supported")] + RecoveryRoleSecurityQuestionsNotSupported, + + #[error("Recovery role password not supported")] + RecoveryRolePasswordNotSupported, + + #[error("Confirmation role cannot contain Trusted Contact")] + ConfirmationRoleTrustedContactNotSupported, +} diff --git a/crates/sargon/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs b/crates/sargon/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs new file mode 100644 index 000000000..087827ae6 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_shield_prerequisites_status.rs @@ -0,0 +1,16 @@ +use crate::prelude::*; + +/// An enum representing the status of the prerequisites for building a Security Shield. +/// This is, whether the user has the necessary factor sources to build a Security Shield. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SecurityShieldPrerequisitesStatus { + /// A Security Shield can be built with the current Factor Sources available. + Sufficient, + + /// At least one hardware Factor Source must be added in order to build a Shield. + /// Note: this doesn't mean that after adding a hardware Factor Source we would have `Sufficient` status. + HardwareRequired, + + /// One more Factor Source, of any category, must be added in order to build a Shield. + AnyRequired, +} diff --git a/crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_id.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_id.rs similarity index 100% rename from crates/sargon/src/profile/mfa/security_structures/factor_source_id_level/security_structure_id.rs rename to crates/sargon/src/profile/mfa/security_structures/security_structure_id.rs diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/abstract_security_structure_of_factors.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/abstract_security_structure_of_factors.rs new file mode 100644 index 000000000..9ba771767 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/abstract_security_structure_of_factors.rs @@ -0,0 +1,47 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AbstractSecurityStructure { + /// Metadata of this Security Structure, such as globally unique and + /// stable identifier, creation date and user chosen label (name). + pub metadata: SecurityStructureMetadata, + + /// The structure of factors to use for certain roles, Primary, Recovery + /// and Confirmation role. + pub matrix_of_factors: AbstractMatrixBuilt, +} + +impl Identifiable for AbstractSecurityStructure { + type ID = ::ID; + + fn id(&self) -> Self::ID { + self.metadata.id() + } +} + +impl AbstractSecurityStructure { + pub fn all_factors(&self) -> HashSet<&FACTOR> { + self.matrix_of_factors.all_factors() + } +} + +impl AbstractSecurityStructure { + pub fn with_metadata( + metadata: SecurityStructureMetadata, + matrix_of_factors: AbstractMatrixBuilt, + ) -> Self { + Self { + metadata, + matrix_of_factors, + } + } + + pub fn new( + display_name: DisplayName, + matrix_of_factors: AbstractMatrixBuilt, + ) -> Self { + let metadata = SecurityStructureMetadata::new(display_name); + Self::with_metadata(metadata, matrix_of_factors) + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/mod.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/mod.rs new file mode 100644 index 000000000..9e2b04e3b --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/mod.rs @@ -0,0 +1,7 @@ +mod abstract_security_structure_of_factors; +mod security_structure_of_factor_source_ids; +mod security_structure_of_factor_sources; + +pub(crate) use abstract_security_structure_of_factors::*; +pub use security_structure_of_factor_source_ids::*; +pub use security_structure_of_factor_sources::*; diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_source_ids.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_source_ids.rs new file mode 100644 index 000000000..0ae91a796 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_source_ids.rs @@ -0,0 +1,218 @@ +use crate::prelude::*; + +pub type SecurityStructureOfFactorSourceIds = + AbstractSecurityStructure; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct SecurityStructureOfFactorInstances { + /// The ID of the `SecurityStructureOfFactorSourceIDs` in + /// `profile.app_preferences.security.security_structures_of_factor_source_ids` + /// which was used to derive the factor instances in this structure. Or rather: + /// The id of `SecurityStructureOfFactorSources`. + pub security_structure_id: SecurityStructureID, + + /// The structure of factors to use for certain roles, Primary, Recovery + /// and Confirmation role. + pub matrix_of_factors: MatrixOfFactorInstances, +} + +impl SecurityStructureOfFactorInstances { + pub fn new( + security_structure_id: SecurityStructureID, + matrix_of_factors: MatrixOfFactorInstances, + ) -> Self { + Self { + security_structure_id, + matrix_of_factors, + } + } +} + +impl Identifiable for SecurityStructureOfFactorInstances { + type ID = ::ID; + + fn id(&self) -> Self::ID { + self.security_structure_id + } +} + +impl HasSampleValues for SecurityStructureOfFactorInstances { + fn sample() -> Self { + Self { + security_structure_id: SecurityStructureID::sample(), + matrix_of_factors: MatrixOfFactorInstances::sample(), + } + } + + fn sample_other() -> Self { + Self { + security_structure_id: SecurityStructureID::sample_other(), + matrix_of_factors: MatrixOfFactorInstances::sample_other(), + } + } +} + +impl HasSampleValues for SecurityStructureOfFactorSourceIds { + fn sample() -> Self { + let metadata = SecurityStructureMetadata::sample(); + Self::with_metadata(metadata, MatrixOfFactorSourceIds::sample()) + } + + fn sample_other() -> Self { + let metadata = SecurityStructureMetadata::sample_other(); + Self::with_metadata(metadata, MatrixOfFactorSourceIds::sample_other()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityStructureOfFactorSourceIds; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn assert_json_sample() { + let sut = SUT::sample(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "metadata": { + "id": "ffffffff-ffff-ffff-ffff-ffffffffffff", + "displayName": "Spending Account", + "createdOn": "2023-09-11T16:05:56.000Z", + "lastUpdatedOn": "2023-09-11T16:05:56.000Z" + }, + "matrixOfFactors": { + "primaryRole": { + "threshold": 2, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + }, + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + } + "#, + ); + } + + #[test] + fn assert_json_sample_other() { + let sut = SUT::sample_other(); + assert_eq_after_json_roundtrip( + &sut, + r#" + { + "metadata": { + "id": "dededede-dede-dede-dede-dededededede", + "displayName": "Savings Account", + "createdOn": "2023-12-24T17:13:56.123Z", + "lastUpdatedOn": "2023-12-24T17:13:56.123Z" + }, + "matrixOfFactors": { + "primaryRole": { + "threshold": 1, + "thresholdFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" + } + } + ], + "overrideFactors": [] + }, + "recoveryRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + } + } + ] + }, + "confirmationRole": { + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ + { + "discriminator": "fromHash", + "fromHash": { + "kind": "ledgerHQHardwareWallet", + "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" + } + } + ] + }, + "numberOfDaysUntilAutoConfirm": 14 + } + } + "#, + ); + } +} diff --git a/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_sources.rs b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_sources.rs new file mode 100644 index 000000000..c9b3f75e1 --- /dev/null +++ b/crates/sargon/src/profile/mfa/security_structures/security_structure_of_factors/security_structure_of_factor_sources.rs @@ -0,0 +1,122 @@ +use crate::prelude::*; + +pub type SecurityStructureOfFactorSources = + AbstractSecurityStructure; + +impl HasSampleValues for SecurityStructureOfFactorSources { + fn sample() -> Self { + let metadata = SecurityStructureMetadata::sample(); + Self::with_metadata(metadata, MatrixOfFactorSources::sample()) + } + + fn sample_other() -> Self { + let metadata = SecurityStructureMetadata::sample_other(); + Self::with_metadata(metadata, MatrixOfFactorSources::sample_other()) + } +} + +pub type MatrixOfFactorSourceIDs = MatrixOfFactorSourceIds; + +impl TryFrom<(&MatrixOfFactorSourceIDs, &FactorSources)> + for MatrixOfFactorSources +{ + type Error = CommonError; + fn try_from( + value: (&MatrixOfFactorSourceIDs, &FactorSources), + ) -> Result { + Self::new(value.0.clone(), value.1) + } +} + +impl TryFrom<(&SecurityStructureOfFactorSourceIDs, &FactorSources)> + for SecurityStructureOfFactorSources +{ + type Error = CommonError; + fn try_from( + value: (&SecurityStructureOfFactorSourceIDs, &FactorSources), + ) -> Result { + let (id_level, factor_sources) = value; + let matrix_of_factors = MatrixOfFactorSources::try_from(( + &id_level.matrix_of_factors, + factor_sources, + ))?; + Ok(Self { + metadata: id_level.metadata.clone(), + matrix_of_factors, + }) + } +} + +impl From + for SecurityStructureOfFactorSourceIDs +{ + fn from(value: SecurityStructureOfFactorSources) -> Self { + Self { + metadata: value.metadata, + matrix_of_factors: value.matrix_of_factors.into(), + } + } +} + +impl + From> + for AbstractRoleBuilderOrBuilt +{ + fn from( + value: AbstractRoleBuilderOrBuilt, + ) -> Self { + Self::with_factors( + value.get_threshold(), + value + .get_threshold_factors() + .iter() + .map(|f| f.factor_source_id()), + value + .get_override_factors() + .iter() + .map(|f| f.factor_source_id()), + ) + } +} + +impl From for MatrixOfFactorSourceIDs { + fn from(value: MatrixOfFactorSources) -> Self { + unsafe { + Self::unbuilt_with_roles_and_days( + PrimaryRoleWithFactorSourceIds::from(value.primary_role), + RecoveryRoleWithFactorSourceIds::from(value.recovery_role), + ConfirmationRoleWithFactorSourceIds::from( + value.confirmation_role, + ), + value.number_of_days_until_auto_confirm, + ) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityStructureOfFactorSources; + + #[test] + fn equality() { + assert_eq!(SUT::sample(), SUT::sample()); + assert_eq!(SUT::sample_other(), SUT::sample_other()); + } + + #[test] + fn inequality() { + assert_ne!(SUT::sample(), SUT::sample_other()); + } + + #[test] + fn test_into_id_level_and_back() { + let factor_sources = FactorSources::sample_values_all(); + let sut = SUT::sample(); + let id_level = SecurityStructureOfFactorSourceIDs::from(sut.clone()); + let detailed = SUT::try_from((&id_level, &factor_sources)).unwrap(); + assert_eq!(detailed, sut); + } +} diff --git a/crates/sargon/src/profile/v100/app_preferences/security.rs b/crates/sargon/src/profile/v100/app_preferences/security.rs index 6a9f5c156..2dcdbfbff 100644 --- a/crates/sargon/src/profile/v100/app_preferences/security.rs +++ b/crates/sargon/src/profile/v100/app_preferences/security.rs @@ -1,5 +1,26 @@ use crate::prelude::*; +pub type SecurityStructureOfFactorSourceIDs = + SecurityStructureOfFactorSourceIds; + +decl_identified_vec_of!( + /// A collection of [`SecurityStructureOfFactorSourceIDs`] + SecurityStructuresOfFactorSourceIDs, + SecurityStructureOfFactorSourceIDs +); + +impl HasSampleValues for SecurityStructuresOfFactorSourceIDs { + fn sample() -> Self { + Self::from_iter([ + SecurityStructureOfFactorSourceIDs::sample(), + SecurityStructureOfFactorSourceIDs::sample_other(), + ]) + } + fn sample_other() -> Self { + Self::from_iter([SecurityStructureOfFactorSourceIDs::sample_other()]) + } +} + /// Controls e.g. if Profile Snapshot gets synced to iCloud or not, and whether /// developer mode is enabled or not. In future (MFA) we will also save a list of /// MFA security structure configurations. @@ -154,7 +175,7 @@ mod tests { assert_eq_after_json_roundtrip( &sut, r#" - { + { "isCloudProfileSyncEnabled": true, "isDeveloperModeEnabled": false, "isAdvancedLockEnabled": false, @@ -166,9 +187,9 @@ mod tests { "createdOn": "2023-09-11T16:05:56.000Z", "lastUpdatedOn": "2023-09-11T16:05:56.000Z" }, - "numberOfEpochsUntilAutoConfirmation": 4032, "matrixOfFactors": { "primaryRole": { + "threshold": 2, "thresholdFactors": [ { "discriminator": "fromHash", @@ -177,23 +198,6 @@ mod tests { "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" } }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "arculusCard", - "body": "12f36554769cd96614776e6dbd5629825b8e87366eec5e515de32bb1ea153820" - } - }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "offDeviceMnemonic", - "body": "820122c9573768ab572b0c9fa492a45b7b451a2740291b3da908ad423d10e410" - } - } - ], - "threshold": 2, - "overrideFactors": [ { "discriminator": "fromHash", "fromHash": { @@ -201,27 +205,20 @@ mod tests { "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" } } - ] + ], + "overrideFactors": [] }, "recoveryRole": { - "thresholdFactors": [ + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ { "discriminator": "fromHash", "fromHash": { - "kind": "arculusCard", - "body": "12f36554769cd96614776e6dbd5629825b8e87366eec5e515de32bb1ea153820" + "kind": "device", + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" } }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "arculusCard", - "body": "3ac064d4b40f78effe7037a12f3287efc67aa87af7c6a083738eae05e28dadaf" - } - } - ], - "threshold": 2, - "overrideFactors": [ { "discriminator": "fromHash", "fromHash": { @@ -232,25 +229,19 @@ mod tests { ] }, "confirmationRole": { - "thresholdFactors": [], "threshold": 0, + "thresholdFactors": [], "overrideFactors": [ { "discriminator": "fromHash", "fromHash": { - "kind": "securityQuestions", - "body": "aabc6041d95785ecfabe7d5ed5af259e20e4e3f5f95b16fdeca386bc75796b46" - } - }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "ledgerHQHardwareWallet", - "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" } } ] - } + }, + "numberOfDaysUntilAutoConfirm": 14 } }, { @@ -260,75 +251,37 @@ mod tests { "createdOn": "2023-12-24T17:13:56.123Z", "lastUpdatedOn": "2023-12-24T17:13:56.123Z" }, - "numberOfEpochsUntilAutoConfirmation": 8064, "matrixOfFactors": { "primaryRole": { + "threshold": 1, "thresholdFactors": [ { "discriminator": "fromHash", "fromHash": { "kind": "device", - "body": "5255999c65076ce9ced5a1881f1a621bba1ce3f1f68a61df462d96822a5190cd" - } - }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "arculusCard", - "body": "3ac064d4b40f78effe7037a12f3287efc67aa87af7c6a083738eae05e28dadaf" - } - }, - { - "discriminator": "fromHash", - "fromHash": { - "kind": "offDeviceMnemonic", - "body": "5c308b9c3e41912d4af4c5ff088e84877aac5de01c95f32dedd280d55a6d8262" + "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" } } ], - "threshold": 2, - "overrideFactors": [ - { - "discriminator": "fromHash", - "fromHash": { - "kind": "ledgerHQHardwareWallet", - "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" - } - } - ] + "overrideFactors": [] }, "recoveryRole": { - "thresholdFactors": [ - { - "discriminator": "fromHash", - "fromHash": { - "kind": "arculusCard", - "body": "3ac064d4b40f78effe7037a12f3287efc67aa87af7c6a083738eae05e28dadaf" - } - } - ], - "threshold": 1, + "threshold": 0, + "thresholdFactors": [], "overrideFactors": [ { "discriminator": "fromHash", "fromHash": { "kind": "ledgerHQHardwareWallet", - "body": "52ef052a0642a94279b296d6b3b17dedc035a7ae37b76c1d60f11f2725100077" + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" } } ] }, "confirmationRole": { - "thresholdFactors": [], "threshold": 0, + "thresholdFactors": [], "overrideFactors": [ - { - "discriminator": "fromHash", - "fromHash": { - "kind": "securityQuestions", - "body": "bb0ac72196f748bba4ddf9c6d87c4e3ea939750e3a207f312653aa25f3f9c060" - } - }, { "discriminator": "fromHash", "fromHash": { @@ -337,7 +290,8 @@ mod tests { } } ] - } + }, + "numberOfDaysUntilAutoConfirm": 14 } } ] diff --git a/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs b/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs index 63ee014ad..240992d17 100644 --- a/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs +++ b/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs @@ -155,7 +155,7 @@ mod tests { assert_eq_after_json_roundtrip( &model, r#" - { + { "discriminator": "securified", "securedEntityControl": { "veci": null, @@ -164,6 +164,7 @@ mod tests { "securityStructureId": "ffffffff-ffff-ffff-ffff-ffffffffffff", "matrixOfFactors": { "primaryRole": { + "threshold": 2, "thresholdFactors": [ { "factorSourceID": { @@ -189,16 +190,13 @@ mod tests { } } } - } - ], - "threshold": 1, - "overrideFactors": [ + }, { "factorSourceID": { "discriminator": "fromHash", "fromHash": { - "kind": "device", - "body": "5255999c65076ce9ced5a1881f1a621bba1ce3f1f68a61df462d96822a5190cd" + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" } }, "badge": { @@ -208,7 +206,7 @@ mod tests { "hierarchicalDeterministicPublicKey": { "publicKey": { "curve": "curve25519", - "compressedData": "e0293d4979bc303ea4fe361a62baf9c060c7d90267972b05c61eead9ef3eed3e" + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" }, "derivationPath": { "scheme": "cap26", @@ -218,10 +216,13 @@ mod tests { } } } - ] + ], + "overrideFactors": [] }, "recoveryRole": { - "thresholdFactors": [ + "threshold": 0, + "thresholdFactors": [], + "overrideFactors": [ { "factorSourceID": { "discriminator": "fromHash", @@ -237,25 +238,22 @@ mod tests { "hierarchicalDeterministicPublicKey": { "publicKey": { "curve": "curve25519", - "compressedData": "161a65a7b4f374d81bf5e7f73669f5b09b684a860812ec1a34f3220b6ffe8dcf" + "compressedData": "427969814e15d74c3ff4d9971465cb709d210c8a7627af9466bdaa67bd0929b7" }, "derivationPath": { "scheme": "cap26", - "path": "m/44H/1022H/1H/525H/1460H/54S" + "path": "m/44H/1022H/1H/525H/1460H/0S" } } } } - } - ], - "threshold": 1, - "overrideFactors": [ + }, { "factorSourceID": { "discriminator": "fromHash", "fromHash": { - "kind": "device", - "body": "5255999c65076ce9ced5a1881f1a621bba1ce3f1f68a61df462d96822a5190cd" + "kind": "ledgerHQHardwareWallet", + "body": "ab59987eedd181fe98e512c1ba0f5ff059f11b5c7c56f15614dcc9fe03fec58b" } }, "badge": { @@ -265,11 +263,11 @@ mod tests { "hierarchicalDeterministicPublicKey": { "publicKey": { "curve": "curve25519", - "compressedData": "23fa85f95c79684d2768f46ec4379b5e031757b727f76cfd01a50bd4cf8afcff" + "compressedData": "92cd6838cd4e7b0523ed93d498e093f71139ffd5d632578189b39a26005be56b" }, "derivationPath": { "scheme": "cap26", - "path": "m/44H/1022H/1H/525H/1460H/237S" + "path": "m/44H/1022H/1H/525H/1460H/0S" } } } @@ -278,41 +276,15 @@ mod tests { ] }, "confirmationRole": { - "thresholdFactors": [ - { - "factorSourceID": { - "discriminator": "fromHash", - "fromHash": { - "kind": "device", - "body": "f1a93d324dd0f2bff89963ab81ed6e0c2ee7e18c0827dc1d3576b2d9f26bbd0a" - } - }, - "badge": { - "discriminator": "virtualSource", - "virtualSource": { - "discriminator": "hierarchicalDeterministicPublicKey", - "hierarchicalDeterministicPublicKey": { - "publicKey": { - "curve": "curve25519", - "compressedData": "0f081cd5f944efc9cae2f3262e30b445b947601b2fc668938a7c4d464c88fe69" - }, - "derivationPath": { - "scheme": "cap26", - "path": "m/44H/1022H/1H/525H/1460H/27S" - } - } - } - } - } - ], - "threshold": 1, + "threshold": 0, + "thresholdFactors": [], "overrideFactors": [ { "factorSourceID": { "discriminator": "fromHash", "fromHash": { - "kind": "device", - "body": "5255999c65076ce9ced5a1881f1a621bba1ce3f1f68a61df462d96822a5190cd" + "kind": "password", + "body": "181ab662e19fac3ad9f08d5c673b286d4a5ed9cd3762356dc9831dc42427c1b9" } }, "badge": { @@ -322,18 +294,19 @@ mod tests { "hierarchicalDeterministicPublicKey": { "publicKey": { "curve": "curve25519", - "compressedData": "d3d66160cf7117b310c7875fbf8b5695ccc13116a167d13196d22dd8be18a60f" + "compressedData": "4af49eb56b1af579aaf03f1760ec526f56e2297651f7a067f4b362f685417a81" }, "derivationPath": { "scheme": "cap26", - "path": "m/44H/1022H/1H/525H/1460H/13S" + "path": "m/44H/1022H/1H/525H/1460H/0S" } } } } } ] - } + }, + "numberOfDaysUntilAutoConfirm": 14 } } } diff --git a/crates/sargon/src/profile/v100/factors/factor_source_category.rs b/crates/sargon/src/profile/v100/factors/factor_source_category.rs new file mode 100644 index 000000000..3d9d4f900 --- /dev/null +++ b/crates/sargon/src/profile/v100/factors/factor_source_category.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +/// An enum representing the **category** of a `FactorSource`/`FactorSourceKind`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FactorSourceCategory { + /// Something I am. + Identity, + + /// Something I have. + Hardware, + + /// Something I know. + Information, + + /// Someone I trust. + Contact, +} diff --git a/crates/sargon/src/profile/v100/factors/factor_source_kind.rs b/crates/sargon/src/profile/v100/factors/factor_source_kind.rs index e4f5a5b91..5d60b958f 100644 --- a/crates/sargon/src/profile/v100/factors/factor_source_kind.rs +++ b/crates/sargon/src/profile/v100/factors/factor_source_kind.rs @@ -104,6 +104,20 @@ impl FactorSourceKind { } } +impl FactorSourceKind { + pub fn category(&self) -> FactorSourceCategory { + use FactorSourceCategory::*; + match self { + Self::LedgerHQHardwareWallet | Self::ArculusCard => Hardware, + Self::Password + | Self::SecurityQuestions + | Self::OffDeviceMnemonic => Information, + Self::Device => Identity, + Self::TrustedContact => Contact, + } + } +} + impl HasSampleValues for FactorSourceKind { fn sample() -> Self { Self::Device @@ -205,6 +219,29 @@ mod tests { assert_eq!(SUT::Password.discriminant(), "password"); } + #[test] + fn category() { + assert_eq!( + SUT::LedgerHQHardwareWallet.category(), + FactorSourceCategory::Hardware + ); + assert_eq!(SUT::ArculusCard.category(), FactorSourceCategory::Hardware); + assert_eq!(SUT::Password.category(), FactorSourceCategory::Information); + assert_eq!( + SUT::SecurityQuestions.category(), + FactorSourceCategory::Information + ); + assert_eq!( + SUT::OffDeviceMnemonic.category(), + FactorSourceCategory::Information + ); + assert_eq!(SUT::Device.category(), FactorSourceCategory::Identity); + assert_eq!( + SUT::TrustedContact.category(), + FactorSourceCategory::Contact + ); + } + #[test] fn display() { assert_eq!(format!("{}", SUT::Device.discriminant()), "device"); diff --git a/crates/sargon/src/profile/v100/factors/is_factor_source.rs b/crates/sargon/src/profile/v100/factors/is_factor_source.rs index 79f6218ce..b25e1d940 100644 --- a/crates/sargon/src/profile/v100/factors/is_factor_source.rs +++ b/crates/sargon/src/profile/v100/factors/is_factor_source.rs @@ -19,6 +19,10 @@ pub trait BaseBaseIsFactorSource { } fn name(&self) -> String; + + fn category(&self) -> FactorSourceCategory { + self.factor_source_kind().category() + } } pub trait BaseIsFactorSource: diff --git a/crates/sargon/src/profile/v100/factors/mod.rs b/crates/sargon/src/profile/v100/factors/mod.rs index f75663847..f70c985bd 100644 --- a/crates/sargon/src/profile/v100/factors/mod.rs +++ b/crates/sargon/src/profile/v100/factors/mod.rs @@ -1,5 +1,6 @@ mod factor_instance; mod factor_source; +mod factor_source_category; mod factor_source_common; mod factor_source_crypto_parameters; mod factor_source_flag; @@ -18,6 +19,7 @@ mod is_factor_source; pub use factor_instance::*; pub use factor_source::*; +pub use factor_source_category::*; pub use factor_source_common::*; pub use factor_source_crypto_parameters::*; pub use factor_source_flag::*; diff --git a/crates/sargon/src/signing/collector/signatures_collector.rs b/crates/sargon/src/signing/collector/signatures_collector.rs index d07686410..80810b950 100644 --- a/crates/sargon/src/signing/collector/signatures_collector.rs +++ b/crates/sargon/src/signing/collector/signatures_collector.rs @@ -1552,16 +1552,13 @@ mod tests { .transaction_signing .factor_instance()]) } - EntitySecurityState::Securified { value } => { - let matrix = value - .security_structure - .matrix_of_factors - .clone(); - let mut set = IndexSet::new(); - set.extend(matrix.primary_role.threshold_factors); - set.extend(matrix.primary_role.override_factors); - set - } + EntitySecurityState::Securified { value } => value + .security_structure + .matrix_of_factors + .all_factors() + .into_iter() + .cloned() + .collect::>(), } } } diff --git a/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs b/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs deleted file mode 100644 index f1af40b14..000000000 --- a/crates/sargon/src/signing/petition_types/general_role_with_hd_factor_instance.rs +++ /dev/null @@ -1,225 +0,0 @@ -use crate::prelude::*; - -decl_role_runtime_kind_with_factors!( - /// A general depiction of each of the roles in a `MatrixOfFactorInstances`. - /// `SignaturesCollector` can work on any `RoleKind` when dealing with a securified entity. - General, - HierarchicalDeterministicFactorInstance -); - -impl HasRoleKindObjectSafe - for GeneralRoleWithHierarchicalDeterministicFactorInstances -{ - fn get_role_kind(&self) -> RoleKind { - self.role - } -} - -impl TryFrom<(MatrixOfFactorInstances, RoleKind)> - for GeneralRoleWithHierarchicalDeterministicFactorInstances -{ - type Error = CommonError; - - fn try_from( - (matrix, role): (MatrixOfFactorInstances, RoleKind), - ) -> Result { - let (threshold_factors, threshold, override_factors) = match role { - RoleKind::Primary => ( - matrix.primary_role.threshold_factors, - matrix.primary_role.threshold, - matrix.primary_role.override_factors, - ), - RoleKind::Recovery => ( - matrix.recovery_role.threshold_factors, - matrix.recovery_role.threshold, - matrix.recovery_role.override_factors, - ), - RoleKind::Confirmation => ( - matrix.confirmation_role.threshold_factors, - matrix.confirmation_role.threshold, - matrix.confirmation_role.override_factors, - ), - }; - - GeneralRoleWithHierarchicalDeterministicFactorInstances::with_factors_and_role( - role, - threshold_factors - .iter() - .map(|f| HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone())) - .collect::>>()?, - threshold, - override_factors - .iter() - .map(|f| HierarchicalDeterministicFactorInstance::try_from_factor_instance(f.clone())) - .collect::>>()?, - ) - } -} - -impl GeneralRoleWithHierarchicalDeterministicFactorInstances { - pub fn single_override( - role: RoleKind, - factor: HierarchicalDeterministicFactorInstance, - ) -> Self { - assert!(factor.is_securified(), "non securified factor"); - Self::with_factors_and_role(role, [], 0, [factor]) - .expect("Zero threshold with zero threshold factors and one override should not fail.") - } - - pub fn single_threshold( - role: RoleKind, - factor: HierarchicalDeterministicFactorInstance, - ) -> Self { - assert!(factor.is_securified(), "non securified factor"); - Self::with_factors_and_role(role, [factor], 1, []).expect( - "Single threshold with one threshold factor should not fail.", - ) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[allow(clippy::upper_case_acronyms)] - type SUT = GeneralRoleWithHierarchicalDeterministicFactorInstances; - - #[test] - fn test_from_primary_role() { - assert_eq!( - SUT::try_from( - (matrix(), RoleKind::Primary) - ).unwrap(), - SUT::with_factors_and_role( - RoleKind::Primary, - [ - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0) - ], - 1, - [] - ).unwrap() - ) - } - - #[test] - fn test_get_role() { - let test = |role: RoleKind| { - let sut = SUT::single_override( - role, - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0) - ); - assert_eq!(sut.role, role); - }; - test(RoleKind::Primary); - test(RoleKind::Confirmation); - test(RoleKind::Recovery); - } - - #[test] - fn test_from_recovery_role() { - let r = recovery_role(); - assert_eq!( - SUT::try_from( - (matrix(), RoleKind::Recovery) - ).unwrap(), - SUT::with_factors_and_role( - RoleKind::Recovery, - r.threshold_factors - .clone() - .into_iter() - .map(|f: FactorInstance| { - HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) - .unwrap() - }) - .collect_vec(), - 1, - [] - ).unwrap() - ) - } - - #[test] - fn test_from_confirmation_role() { - let r = confirmation_role(); - assert_eq!( - SUT::try_from((matrix(), RoleKind::Confirmation)).unwrap(), - SUT::with_factors_and_role( - RoleKind::Confirmation, - r.threshold_factors - .clone() - .into_iter() - .map(|f: FactorInstance| { - HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) - .unwrap() - }) - .collect_vec(), - r.threshold, - r.override_factors - .clone() - .into_iter() - .map(|f: FactorInstance| { - HierarchicalDeterministicFactorInstance::try_from_factor_instance(f) - .unwrap() - }) - .collect_vec() - ) - .unwrap() - ) - } - - #[test] - fn test_from_matrix_containing_physical_badge() { - let matrix = MatrixOfFactorInstances::new( - PrimaryRoleWithFactorInstances::new( - [FactorInstance::sample_other()], - 1, - [], - ) - .unwrap(), - recovery_role(), - confirmation_role(), - ) - .unwrap(); - - assert_eq!( - SUT::try_from((matrix, RoleKind::Primary)), - Err(CommonError::BadgeIsNotVirtualHierarchicalDeterministic) - ); - } - - fn matrix() -> MatrixOfFactorInstances { - MatrixOfFactorInstances::new( - primary_role(), - recovery_role(), - confirmation_role(), - ) - .unwrap() - } - - fn primary_role() -> PrimaryRoleWithFactorInstances { - PrimaryRoleWithFactorInstances::new([HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(0).into()], 1, []) - .unwrap() - } - - fn recovery_role() -> RecoveryRoleWithFactorInstances { - RecoveryRoleWithFactorInstances::new( - [ - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(1).into() - ], - 1, - [], - ) - .unwrap() - } - - fn confirmation_role() -> ConfirmationRoleWithFactorInstances { - ConfirmationRoleWithFactorInstances::new( - [ - HierarchicalDeterministicFactorInstance::sample_mainnet_account_device_factor_fs_0_securified_at_index(2).into() - ], - 1, - [], - ) - .unwrap() - } -} diff --git a/crates/sargon/src/signing/petition_types/mod.rs b/crates/sargon/src/signing/petition_types/mod.rs index a7f2827d3..406165902 100644 --- a/crates/sargon/src/signing/petition_types/mod.rs +++ b/crates/sargon/src/signing/petition_types/mod.rs @@ -1,5 +1,4 @@ mod factor_list_kind; -mod general_role_with_hd_factor_instance; mod petition_for_entity; mod petition_for_factors_types; mod petition_for_transaction; @@ -13,6 +12,5 @@ pub(crate) use petition_for_transaction::*; pub(crate) use petition_status::*; pub(crate) use petitions::*; -pub use general_role_with_hd_factor_instance::*; pub use petition_for_factors_types::*; pub use role_kind::*; diff --git a/crates/sargon/src/signing/petition_types/petition_for_entity.rs b/crates/sargon/src/signing/petition_types/petition_for_entity.rs index fc499ab27..e2e600e0b 100644 --- a/crates/sargon/src/signing/petition_types/petition_for_entity.rs +++ b/crates/sargon/src/signing/petition_types/petition_for_entity.rs @@ -101,11 +101,11 @@ impl PetitionForEntity { payload_id, entity, PetitionForFactors::new_threshold( - role_with_factor_instances.threshold_factors, - role_with_factor_instances.threshold as i8, + role_with_factor_instances.get_threshold_factors(), + role_with_factor_instances.get_threshold() as i8, ), PetitionForFactors::new_override( - role_with_factor_instances.override_factors, + role_with_factor_instances.get_override_factors(), ), ) } @@ -626,45 +626,6 @@ mod tests { sut.add_signature(HDSignature::sample()); } - #[test] - fn factor_should_not_be_used_in_both_lists() { - let fi = HierarchicalDeterministicFactorInstance::sample_id_to_instance( - CAP26EntityKind::Account, - Hardened::from_local_key_space(0, IsSecurified(true)).unwrap(), - ); - assert_eq!( - GeneralRoleWithHierarchicalDeterministicFactorInstances::with_factors_and_role( - RoleKind::Primary, - [FactorSourceIDFromHash::sample_at(0)].map(&fi), - 1, - [FactorSourceIDFromHash::sample_at(0)].map(&fi), - ), - Err(CommonError::InvalidSecurityStructureFactorInBothThresholdAndOverride) - ); - } - - #[test] - fn threshold_should_not_be_bigger_than_threshold_factors() { - let fi = HierarchicalDeterministicFactorInstance::sample_id_to_instance( - CAP26EntityKind::Account, - Hardened::from_local_key_space(0, IsSecurified(true)).unwrap(), - ); - assert_eq!( - GeneralRoleWithHierarchicalDeterministicFactorInstances::with_factors_and_role( - RoleKind::Primary, - [FactorSourceIDFromHash::sample_at(0)].map(&fi), - 2, - [], - ), - Err( - CommonError::InvalidSecurityStructureThresholdExceedsFactors { - threshold: 2, - factors: 1, - } - ) - ); - } - #[test] #[should_panic] fn cannot_add_same_signature_twice() { diff --git a/crates/sargon/src/system/sargon_os/sargon_os_security_structures.rs b/crates/sargon/src/system/sargon_os/sargon_os_security_structures.rs index 069274af6..7a809c0d0 100644 --- a/crates/sargon/src/system/sargon_os/sargon_os_security_structures.rs +++ b/crates/sargon/src/system/sargon_os/sargon_os_security_structures.rs @@ -100,6 +100,21 @@ impl SargonOS { } } +impl SargonOS { + /// Returns the status of the prerequisites for building a Security Shield. + /// + /// According to [definition][doc], a Security Shield can be built if the user has, asides from + /// the Identity factor, "2 or more factors, one of which must be Hardware" + /// + /// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3758063620/MFA+Rules+for+Factors+and+Security+Shields#Factor-Prerequisites + pub fn security_shield_prerequisites_status( + &self, + ) -> Result { + self.profile_state_holder + .access_profile_with(|p| p.security_shield_prerequisites_status()) + } +} + #[cfg(test)] mod tests { @@ -324,4 +339,11 @@ mod tests { assert_eq!(sources_by_id_lookup, structures); } + + #[actix_rt::test] + async fn security_shield_prerequisites_status() { + let os = SUT::fast_boot().await; + let result = os.security_shield_prerequisites_status().unwrap(); + assert_eq!(result, SecurityShieldPrerequisitesStatus::HardwareRequired); + } } diff --git a/crates/sargon/src/types/samples/account_samples.rs b/crates/sargon/src/types/samples/account_samples.rs index 64c1213db..8c718a7ba 100644 --- a/crates/sargon/src/types/samples/account_samples.rs +++ b/crates/sargon/src/types/samples/account_samples.rs @@ -157,40 +157,20 @@ impl Account { make_role: impl Fn() -> GeneralRoleWithHierarchicalDeterministicFactorInstances, ) -> Self { let role = make_role(); + assert_eq!(role.get_role_kind(), RoleKind::Primary, "If this tests fails you can update the code below to not be hardcoded to set the primary role..."); + let mut matrix = MatrixOfFactorInstances::sample(); + matrix.primary_role = PrimaryRoleWithFactorInstances::with_factors( + role.get_threshold(), + role.get_threshold_factors() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + role.get_override_factors() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + ); - let threshold_factors = role - .threshold_factors - .iter() - .map(|hd| hd.factor_instance()) - .collect::>(); - - let override_factors = role - .override_factors - .iter() - .map(|hd| hd.factor_instance()) - .collect::>(); - - let matrix = MatrixOfFactorInstances::new( - PrimaryRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - RecoveryRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - ConfirmationRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - ) - .unwrap(); let network_id = NetworkID::Mainnet; let address = AccountAddress::new(veci.public_key(), NetworkID::Mainnet); diff --git a/crates/sargon/src/types/samples/persona_samples.rs b/crates/sargon/src/types/samples/persona_samples.rs index 15061d72a..278b7dd69 100644 --- a/crates/sargon/src/types/samples/persona_samples.rs +++ b/crates/sargon/src/types/samples/persona_samples.rs @@ -122,40 +122,19 @@ impl Persona { make_role: impl Fn() -> GeneralRoleWithHierarchicalDeterministicFactorInstances, ) -> Self { let role = make_role(); - - let threshold_factors = role - .threshold_factors - .iter() - .map(|hd| hd.factor_instance()) - .collect::>(); - - let override_factors = role - .override_factors - .iter() - .map(|hd| hd.factor_instance()) - .collect::>(); - - let matrix = MatrixOfFactorInstances::new( - PrimaryRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - RecoveryRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - ConfirmationRoleWithFactorInstances::new( - threshold_factors.clone(), - role.threshold, - override_factors.clone(), - ) - .unwrap(), - ) - .unwrap(); + assert_eq!(role.get_role_kind(), RoleKind::Primary, "If this tests fails you can update the code below to not be hardcoded to set the primary role..."); + let mut matrix = MatrixOfFactorInstances::sample(); + matrix.primary_role = PrimaryRoleWithFactorInstances::with_factors( + role.get_threshold(), + role.get_threshold_factors() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + role.get_override_factors() + .into_iter() + .map(FactorInstance::from) + .collect_vec(), + ); let address = IdentityAddress::new(veci.public_key(), NetworkID::Mainnet); Self { diff --git a/crates/sargon/tests/vectors/main.rs b/crates/sargon/tests/vectors/main.rs index e2402dd5a..8c97fbd54 100644 --- a/crates/sargon/tests/vectors/main.rs +++ b/crates/sargon/tests/vectors/main.rs @@ -395,7 +395,7 @@ mod encrypted_profile_tests { .map(|x| x.security_state) .for_each(test); - Ok(()) + Ok::<(), CommonError>(()) })?; Ok(()) } diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/storage/KeystoreAccessRequestTest.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/storage/KeystoreAccessRequestTest.kt index 596ab18df..ffe3a2367 100644 --- a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/storage/KeystoreAccessRequestTest.kt +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/storage/KeystoreAccessRequestTest.kt @@ -48,29 +48,29 @@ class KeystoreAccessRequestTest { } } - @Test - fun testSpecsOfMnemonic() = runTest { - val request = KeystoreAccessRequest.ForMnemonic( - onRequestAuthorization = { Result.success(Unit) } - ) - assertInstanceOf(KeySpec.Mnemonic::class.java, request.keySpec) - - try { - request.requestAuthorization().getOrThrow() - } catch (exception: Exception) { - assert(false) { "requestAuthorization for Mnemonic should succeed but didn't" } - } - - val failingRequest = KeystoreAccessRequest.ForMnemonic( - onRequestAuthorization = { Result.failure(RuntimeException("An error")) } - ) - - try { - failingRequest.requestAuthorization().getOrThrow() - assert(false) { "requestAuthorization for failing access to Mnemonic should fail but succeeded" } - } catch (exception: Exception) { - assert(true) { "requestAuthorization for failing access to Mnemonic should fail" } - } - } + // @Test + // fun testSpecsOfMnemonic() = runTest { + // val request = KeystoreAccessRequest.ForMnemonic( + // onRequestAuthorization = { Result.success(Unit) } + // ) + // assertInstanceOf(KeySpec.Mnemonic::class.java, request.keySpec) + + // try { + // request.requestAuthorization().getOrThrow() + // } catch (exception: Exception) { + // assert(false) { "requestAuthorization for Mnemonic should succeed but didn't" } + // } + + // val failingRequest = KeystoreAccessRequest.ForMnemonic( + // onRequestAuthorization = { Result.failure(RuntimeException("An error")) } + // ) + + // try { + // failingRequest.requestAuthorization().getOrThrow() + // assert(false) { "requestAuthorization for failing access to Mnemonic should fail but succeeded" } + // } catch (exception: Exception) { + // assert(true) { "requestAuthorization for failing access to Mnemonic should fail" } + // } + // } } \ No newline at end of file