From 51c15f18f533f2ba8c91043f0c33f3f309d050eb Mon Sep 17 00:00:00 2001 From: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:39:55 +0100 Subject: [PATCH] Entities linked to Factor Source (#302) * WIP * WIP * key mapping solution * using accessibility * rename to linkedTo * WIP * rename to integrity * WIP * missing tests * version bump * changes after feedback * bump again * implemented containsDataForKey in the AndroidStorageDriver for the Android SargonOS * updated Fakes in unit tests * changes after feedback --------- Co-authored-by: giannis.tsepas --- Cargo.lock | 4 +- ...ecureTestOnlyEphemeral_SecureStorage.swift | 4 + .../Drivers/TestDrivers/TestDrivers.swift | 1 + .../UnsafeStorageDriver+UserDefaults.swift | 26 +- .../System/BIOS/BIOS+Swiftified.swift | 2 + .../System/Drivers/Drivers+Swiftified.swift | 6 +- apple/Sources/Sargon/SargonOS/TestOS.swift | 2 + .../DriversTests/DriversTests.swift | 1 + .../UnsafeStorageDriverTests.swift | 20 ++ .../IntegrationTests/SargonOS/BIOSTests.swift | 3 +- crates/sargon-uniffi/Cargo.toml | 2 +- .../entities_linked_to_factor_source.rs | 21 ++ .../integrity/device.rs | 15 + .../integrity/integrity.rs | 10 + .../integrity/mod.rs | 5 + .../entities_linked_to_factor_source/mod.rs | 7 + .../profile_to_check.rs | 14 + crates/sargon-uniffi/src/profile/v100/mod.rs | 2 + .../secure_storage_driver.rs | 15 + .../sargon-uniffi/src/system/sargon_os/mod.rs | 2 + ...gon_os_entities_linked_to_factor_source.rs | 19 ++ crates/sargon/Cargo.toml | 2 +- .../logic/account/accounts_visibility.rs | 19 ++ .../profile/logic/account/query_accounts.rs | 15 + .../profile/logic/persona/query_personas.rs | 28 ++ .../src/profile/logic/profile_network/mod.rs | 2 + ...etwork_entities_linked_to_factor_source.rs | 105 ++++++ .../profile_network_get_entities.rs | 12 +- .../matrices/matrix_of_factor_instances.rs | 7 + .../entities_linked_to_factor_source.rs | 79 +++++ .../integrity/device.rs | 57 ++++ .../integrity/integrity.rs | 63 ++++ .../integrity/mod.rs | 5 + .../entities_linked_to_factor_source/mod.rs | 7 + .../profile_to_check.rs | 42 +++ .../entity_security_state.rs | 34 ++ crates/sargon/src/profile/v100/mod.rs | 2 + .../secure_storage_client.rs | 42 +++ .../unsafe_storage_client.rs | 176 ++++++++++ crates/sargon/src/system/drivers/drivers.rs | 17 + .../secure_storage_driver.rs | 5 + .../support/test/ephemeral_secure_storage.rs | 10 + .../support/test/fail_secure_storage.rs | 7 + .../unsafe_storage_driver/support/test/mod.rs | 1 - crates/sargon/src/system/sargon_os/mod.rs | 2 + ...gon_os_entities_linked_to_factor_source.rs | 317 ++++++++++++++++++ .../sargon/os/driver/AndroidStorageDriver.kt | 9 + .../sargon/os/storage/StorageUtils.kt | 6 + .../os/storage/key/ByteArrayKeyMapping.kt | 6 + .../os/storage/key/DatastoreKeyMapping.kt | 1 + .../DeviceFactorSourceMnemonicKeyMapping.kt | 10 +- .../sargon/os/storage/key/HostIdKeyMapping.kt | 3 + .../storage/key/ProfileSnapshotKeyMapping.kt | 4 + .../com/radixdlt/sargon/os/driver/Fakes.kt | 4 + 54 files changed, 1265 insertions(+), 15 deletions(-) create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/mod.rs create mode 100644 crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs create mode 100644 crates/sargon-uniffi/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs create mode 100644 crates/sargon/src/profile/logic/profile_network/profile_network_entities_linked_to_factor_source.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/mod.rs create mode 100644 crates/sargon/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs create mode 100644 crates/sargon/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs diff --git a/Cargo.lock b/Cargo.lock index 452b558b0..29b89569d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,7 +2772,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.84" +version = "1.1.85" dependencies = [ "actix-rt", "aes-gcm", @@ -2827,7 +2827,7 @@ dependencies = [ [[package]] name = "sargon-uniffi" -version = "1.1.84" +version = "1.1.85" dependencies = [ "actix-rt", "assert-json-diff", diff --git a/apple/Sources/Sargon/Drivers/TestDrivers/SecureStorage/SecureStorageDriver+InsecureTestOnlyEphemeral_SecureStorage.swift b/apple/Sources/Sargon/Drivers/TestDrivers/SecureStorage/SecureStorageDriver+InsecureTestOnlyEphemeral_SecureStorage.swift index 8289be270..9e6de1857 100644 --- a/apple/Sources/Sargon/Drivers/TestDrivers/SecureStorage/SecureStorageDriver+InsecureTestOnlyEphemeral_SecureStorage.swift +++ b/apple/Sources/Sargon/Drivers/TestDrivers/SecureStorage/SecureStorageDriver+InsecureTestOnlyEphemeral_SecureStorage.swift @@ -25,5 +25,9 @@ extension Insecure︕!TestOnly︕!Ephemeral︕!SecureStorage: SecureStorag public func deleteDataForKey(key: SecureStorageKey) async throws { dictionary.removeValue(forKey: key) } + + public func containsDataForKey(key: SecureStorageKey) async throws -> Bool { + dictionary.keys.contains(key) + } } #endif diff --git a/apple/Sources/Sargon/Drivers/TestDrivers/TestDrivers.swift b/apple/Sources/Sargon/Drivers/TestDrivers/TestDrivers.swift index e8888b383..45de0630c 100644 --- a/apple/Sources/Sargon/Drivers/TestDrivers/TestDrivers.swift +++ b/apple/Sources/Sargon/Drivers/TestDrivers/TestDrivers.swift @@ -11,6 +11,7 @@ extension BIOS { BIOS( bundle: bundle, userDefaultsSuite: userDefaultsSuite, + unsafeStorageKeyMapping: [:], secureStorageDriver: Insecure︕!TestOnly︕!Ephemeral︕!SecureStorage( keychainService: "test" ) diff --git a/apple/Sources/Sargon/Drivers/UnsafeStorage/UnsafeStorageDriver+UserDefaults.swift b/apple/Sources/Sargon/Drivers/UnsafeStorage/UnsafeStorageDriver+UserDefaults.swift index 00f29187a..497e6a90a 100644 --- a/apple/Sources/Sargon/Drivers/UnsafeStorage/UnsafeStorageDriver+UserDefaults.swift +++ b/apple/Sources/Sargon/Drivers/UnsafeStorage/UnsafeStorageDriver+UserDefaults.swift @@ -12,14 +12,22 @@ extension UnsafeStorageDriver where Self == UnsafeStorage { public static var shared: Self { Self.shared } } +public typealias UnsafeStorageKeyMapping = [UnsafeStorageKey: String] + // MARK: - UnsafeStorage /// An `UnsafeStorageDriver` implementation which /// wraps `UserDefaults`. public final class UnsafeStorage: Sendable { public typealias Key = UnsafeStorageKey fileprivate let userDefaults: UserDefaults - public init(userDefaults: UserDefaults = .standard) { + + /// A dictionary containing the custom String value used for a given `UnsafeStorageKey`. + /// This is necessary since some UserDefaults were saved by the Host apps prior to Sargon. + fileprivate let keyMapping: [UnsafeStorageKey: String] + + public init(userDefaults: UserDefaults = .standard, keyMapping: [UnsafeStorageKey: String] = [:]) { self.userDefaults = userDefaults + self.keyMapping = keyMapping } /// Singleton `UnsafeStorageDriver` of type `UnsafeStorage, @@ -30,7 +38,7 @@ public final class UnsafeStorage: Sendable { extension UnsafeStorageKey { /// Translates this `UnsafeStorageKey` into a String /// identifier which we can use with `UserDefaults` - var identifier: String { + public var identifier: String { unsafeStorageKeyIdentifier(key: self) } } @@ -38,14 +46,22 @@ extension UnsafeStorageKey { // MARK: - UnsafeStorage + UnsafeStorageDriver extension UnsafeStorage: UnsafeStorageDriver { public func loadData(key: Key) -> Data? { - userDefaults.data(forKey: key.identifier) + userDefaults.data(forKey: identifier(for: key)) } public func saveData(key: Key, data: Data) { - userDefaults.setValue(data, forKey: key.identifier) + userDefaults.setValue(data, forKey: identifier(for: key)) } public func deleteDataForKey(key: Key) { - userDefaults.removeObject(forKey: key.identifier) + userDefaults.removeObject(forKey: identifier(for: key)) + } + + private func identifier(for key: Key) -> String { + if let mapped = keyMapping[key] { + mapped + } else { + key.identifier + } } } diff --git a/apple/Sources/Sargon/Extensions/Swiftified/System/BIOS/BIOS+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/System/BIOS/BIOS+Swiftified.swift index 82ba53526..6c2cf8083 100644 --- a/apple/Sources/Sargon/Extensions/Swiftified/System/BIOS/BIOS+Swiftified.swift +++ b/apple/Sources/Sargon/Extensions/Swiftified/System/BIOS/BIOS+Swiftified.swift @@ -10,11 +10,13 @@ extension BIOS { public convenience init( bundle: Bundle, userDefaultsSuite: String, + unsafeStorageKeyMapping: UnsafeStorageKeyMapping, secureStorageDriver: SecureStorageDriver ) { let drivers = Drivers( bundle: bundle, userDefaultsSuite: userDefaultsSuite, + unsafeStorageKeyMapping: unsafeStorageKeyMapping, secureStorageDriver: secureStorageDriver ) // https://en.wikipedia.org/wiki/Power-on_self-test diff --git a/apple/Sources/Sargon/Extensions/Swiftified/System/Drivers/Drivers+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/System/Drivers/Drivers+Swiftified.swift index 5bfe9c472..3956e12d6 100644 --- a/apple/Sources/Sargon/Extensions/Swiftified/System/Drivers/Drivers+Swiftified.swift +++ b/apple/Sources/Sargon/Extensions/Swiftified/System/Drivers/Drivers+Swiftified.swift @@ -8,11 +8,13 @@ extension Drivers { public convenience init( bundle: Bundle, userDefaultsSuite: String, + unsafeStorageKeyMapping: UnsafeStorageKeyMapping, secureStorageDriver: SecureStorageDriver ) { self.init( appVersion: (bundle.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "Unknown", userDefaultsSuite: userDefaultsSuite, + unsafeStorageKeyMapping: unsafeStorageKeyMapping, secureStorageDriver: secureStorageDriver ) } @@ -20,13 +22,15 @@ extension Drivers { public convenience init( appVersion: String, userDefaultsSuite: String, + unsafeStorageKeyMapping: UnsafeStorageKeyMapping, secureStorageDriver: SecureStorageDriver ) { self.init( secureStorage: secureStorageDriver, hostInfo: AppleHostInfoDriver(appVersion: appVersion), unsafeStorage: UnsafeStorage( - userDefaults: .init(suiteName: userDefaultsSuite)! + userDefaults: .init(suiteName: userDefaultsSuite)!, + keyMapping: unsafeStorageKeyMapping ) ) } diff --git a/apple/Sources/Sargon/SargonOS/TestOS.swift b/apple/Sources/Sargon/SargonOS/TestOS.swift index 4ec6d4c8d..33751d5c3 100644 --- a/apple/Sources/Sargon/SargonOS/TestOS.swift +++ b/apple/Sources/Sargon/SargonOS/TestOS.swift @@ -7,11 +7,13 @@ extension BIOS { public static func test( bundle: Bundle = .main, userDefaultsSuite: String = "Test", + unsafeStorageKeyMapping: UnsafeStorageKeyMapping = [:], secureStorageDriver: SecureStorageDriver ) -> BIOS { BIOS( bundle: bundle, userDefaultsSuite: userDefaultsSuite, + unsafeStorageKeyMapping: unsafeStorageKeyMapping, secureStorageDriver: secureStorageDriver ) } diff --git a/apple/Tests/IntegrationTests/DriversTests/DriversTests.swift b/apple/Tests/IntegrationTests/DriversTests/DriversTests.swift index ad56ec086..be195839d 100644 --- a/apple/Tests/IntegrationTests/DriversTests/DriversTests.swift +++ b/apple/Tests/IntegrationTests/DriversTests/DriversTests.swift @@ -10,6 +10,7 @@ extension Drivers { Drivers( appVersion: "0.0.1", userDefaultsSuite: "works.rdx", + unsafeStorageKeyMapping: [:], secureStorageDriver: Insecure︕!TestOnly︕!Ephemeral︕!SecureStorage( keychainService: "test" ) diff --git a/apple/Tests/IntegrationTests/DriversTests/UnsafeStorageDriverTests.swift b/apple/Tests/IntegrationTests/DriversTests/UnsafeStorageDriverTests.swift index 61c034db8..e6018e793 100644 --- a/apple/Tests/IntegrationTests/DriversTests/UnsafeStorageDriverTests.swift +++ b/apple/Tests/IntegrationTests/DriversTests/UnsafeStorageDriverTests.swift @@ -14,4 +14,24 @@ class UnsafeStorageDriverTests: DriverTest { sut.deleteDataForKey(key: key) XCTAssertNil(sut.loadData(key: key)) } + + func test_keyMapping() async throws { + // Set up SUT with keyMapping that uses custom key + let key = "custom_key" + let keyMapping: UnsafeStorageKeyMapping = [.factorSourceUserHasWrittenDown: key] + let data = Data([0x01]) + let sut = SUT(keyMapping: keyMapping) + + // Make sure there is no value on UserDefault for custom key + UserDefaults.standard.removeObject(forKey: key) + XCTAssertNil(UserDefaults.standard.value(forKey: key)) + + // Save the value via SUT + sut.saveData(key: .factorSourceUserHasWrittenDown, data: data) + + // Verify the data is saved on UserDefaults using custom key, and not with the one Sargon defines + XCTAssertEqual(sut.loadData(key: .factorSourceUserHasWrittenDown), data) + XCTAssertEqual(UserDefaults.standard.data(forKey: key), data) + XCTAssertNil(UserDefaults.standard.data(forKey: UnsafeStorageKey.factorSourceUserHasWrittenDown.identifier)) + } } diff --git a/apple/Tests/IntegrationTests/SargonOS/BIOSTests.swift b/apple/Tests/IntegrationTests/SargonOS/BIOSTests.swift index c91108a9b..78651f50c 100644 --- a/apple/Tests/IntegrationTests/SargonOS/BIOSTests.swift +++ b/apple/Tests/IntegrationTests/SargonOS/BIOSTests.swift @@ -8,11 +8,12 @@ extension BIOS { static func creatingShared( bundle: Bundle = .main, userDefaultsSuite: String = "test", + unsafeStorageKeyMapping: UnsafeStorageKeyMapping = [:], secureStorageDriver: SecureStorageDriver = Insecure︕!TestOnly︕!Ephemeral︕!SecureStorage( keychainService: "test" ) ) -> BIOS { - creatingShared(drivers: .init(bundle: bundle, userDefaultsSuite: userDefaultsSuite, secureStorageDriver: secureStorageDriver)) + creatingShared(drivers: .init(bundle: bundle, userDefaultsSuite: userDefaultsSuite, unsafeStorageKeyMapping: unsafeStorageKeyMapping, secureStorageDriver: secureStorageDriver)) } } diff --git a/crates/sargon-uniffi/Cargo.toml b/crates/sargon-uniffi/Cargo.toml index 0d2cd7a2d..81d0cd564 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.84" +version = "1.1.85" edition = "2021" build = "build.rs" diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs new file mode 100644 index 000000000..8cb59706b --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs @@ -0,0 +1,21 @@ +use crate::prelude::*; +use sargon::EntitiesLinkedToFactorSource as InternalEntitiesLinkedToFactorSource; + +/// This is the result of checking what entities are controlled by a given `FactorSource`. +#[derive(Clone, PartialEq, InternalConversion, uniffi::Record)] +pub struct EntitiesLinkedToFactorSource { + /// The integrity of the factor source. + pub integrity: FactorSourceIntegrity, + + /// The visible accounts linked to the factor source. + pub accounts: Vec, + + /// The hidden accounts linked to the factor source. + pub hidden_accounts: Vec, + + /// The visible personas linked to the factor source. + pub personas: Vec, + + /// The hidden personas linked to the factor source. + pub hidden_personas: Vec, +} diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs new file mode 100644 index 000000000..29ef3378b --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs @@ -0,0 +1,15 @@ +use crate::prelude::*; +use sargon::DeviceFactorSourceIntegrity as InternalDeviceFactorSourceIntegrity; + +/// A struct representing the integrity of a device factor source. +#[derive(Clone, PartialEq, Eq, InternalConversion, uniffi::Record)] +pub struct DeviceFactorSourceIntegrity { + /// The factor source that is linked to the entities. + pub factor_source: DeviceFactorSource, + + /// Whether the mnemonic of the factor source is present in secure storage. + pub is_mnemonic_present_in_secure_storage: bool, + + /// Whether the mnemonic of the factor source is marked as backed up. + pub is_mnemonic_marked_as_backed_up: bool, +} diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs new file mode 100644 index 000000000..4883614ab --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs @@ -0,0 +1,10 @@ +use crate::prelude::*; +use sargon::FactorSourceIntegrity as InternalFactorSourceIntegrity; + +/// An enum representing the integrity of a factor source. +#[derive(Clone, PartialEq, InternalConversion, uniffi::Enum)] +pub enum FactorSourceIntegrity { + Device(DeviceFactorSourceIntegrity), + + Ledger(LedgerHardwareWalletFactorSource), +} diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs new file mode 100644 index 000000000..4f95cebd8 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs @@ -0,0 +1,5 @@ +mod device; +mod integrity; + +pub use device::*; +pub use integrity::*; diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/mod.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/mod.rs new file mode 100644 index 000000000..bc45f7458 --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/mod.rs @@ -0,0 +1,7 @@ +mod entities_linked_to_factor_source; +mod integrity; +mod profile_to_check; + +pub use entities_linked_to_factor_source::*; +pub use integrity::*; +pub use profile_to_check::*; diff --git a/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs new file mode 100644 index 000000000..f4d52395d --- /dev/null +++ b/crates/sargon-uniffi/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs @@ -0,0 +1,14 @@ +use crate::prelude::*; +use sargon::ProfileToCheck as InternalProfileToCheck; + +/// The Profile to which we want to check the entities linked to a factor source. +#[derive(Clone, PartialEq, InternalConversion, uniffi::Enum)] +#[allow(clippy::large_enum_variant)] +pub enum ProfileToCheck { + /// We should check against the current Profile. + Current, + + /// We should check against a specific Profile. + /// Useful when we are in the Import Mnenmonics flow. + Specific(Profile), +} diff --git a/crates/sargon-uniffi/src/profile/v100/mod.rs b/crates/sargon-uniffi/src/profile/v100/mod.rs index ab26478fb..3c62257e1 100644 --- a/crates/sargon-uniffi/src/profile/v100/mod.rs +++ b/crates/sargon-uniffi/src/profile/v100/mod.rs @@ -1,5 +1,6 @@ mod address; mod app_preferences; +mod entities_linked_to_factor_source; mod entity; mod entity_security_state; mod factors; @@ -11,6 +12,7 @@ mod profile_file_contents; pub use address::*; pub use app_preferences::*; +pub use entities_linked_to_factor_source::*; pub use entity::*; pub use entity_security_state::*; pub use factors::*; diff --git a/crates/sargon-uniffi/src/system/drivers/secure_storage_driver/secure_storage_driver.rs b/crates/sargon-uniffi/src/system/drivers/secure_storage_driver/secure_storage_driver.rs index a0622b141..b23ae2b5b 100644 --- a/crates/sargon-uniffi/src/system/drivers/secure_storage_driver/secure_storage_driver.rs +++ b/crates/sargon-uniffi/src/system/drivers/secure_storage_driver/secure_storage_driver.rs @@ -19,6 +19,11 @@ pub trait SecureStorageDriver: Send + Sync + std::fmt::Debug { ) -> Result<()>; async fn delete_data_for_key(&self, key: SecureStorageKey) -> Result<()>; + + async fn contains_data_for_key( + &self, + key: SecureStorageKey, + ) -> Result; } #[derive(Debug)] @@ -58,4 +63,14 @@ impl InternalSecureStorageDriver for SecureStorageDriverAdapter { .await .into_internal_result() } + + async fn contains_data_for_key( + &self, + key: InternalSecureStorageKey, + ) -> InternalResult { + self.wrapped + .contains_data_for_key(key.into()) + .await + .into_internal_result() + } } diff --git a/crates/sargon-uniffi/src/system/sargon_os/mod.rs b/crates/sargon-uniffi/src/system/sargon_os/mod.rs index b19a85375..e905cc57e 100644 --- a/crates/sargon-uniffi/src/system/sargon_os/mod.rs +++ b/crates/sargon-uniffi/src/system/sargon_os/mod.rs @@ -3,6 +3,7 @@ mod pre_authorization; mod profile_state_holder; mod sargon_os; mod sargon_os_accounts; +mod sargon_os_entities_linked_to_factor_source; mod sargon_os_factors; mod sargon_os_gateway; mod sargon_os_profile; @@ -17,6 +18,7 @@ pub use pre_authorization::*; pub use profile_state_holder::*; pub use sargon_os::*; pub use sargon_os_accounts::*; +pub use sargon_os_entities_linked_to_factor_source::*; pub use sargon_os_factors::*; pub use sargon_os_gateway::*; pub use sargon_os_profile::*; diff --git a/crates/sargon-uniffi/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs new file mode 100644 index 000000000..b429f371f --- /dev/null +++ b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs @@ -0,0 +1,19 @@ +use crate::prelude::*; + +#[uniffi::export] +impl SargonOS { + /// Returns the entities linked to a given `FactorSource`, either on the current `Profile` or a specific one. + pub async fn entities_linked_to_factor_source( + &self, + factor_source: FactorSource, + profile_to_check: ProfileToCheck, + ) -> Result { + self.wrapped + .entities_linked_to_factor_source( + factor_source.into_internal(), + profile_to_check.into_internal(), + ) + .await + .into_result() + } +} diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 76e3c5bb0..94cc008fc 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.84" +version = "1.1.85" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/profile/logic/account/accounts_visibility.rs b/crates/sargon/src/profile/logic/account/accounts_visibility.rs index 8222c04dd..156d69e17 100644 --- a/crates/sargon/src/profile/logic/account/accounts_visibility.rs +++ b/crates/sargon/src/profile/logic/account/accounts_visibility.rs @@ -7,6 +7,13 @@ impl Accounts { .filter(|p| !p.is_hidden() && !p.is_tombstoned()) .collect() } + + pub fn hidden(&self) -> Self { + self.clone() + .into_iter() + .filter(|p| p.is_hidden() && !p.is_tombstoned()) + .collect() + } } #[cfg(test)] @@ -33,4 +40,16 @@ mod tests { assert_eq!(sut.visible(), SUT::just(Account::sample_mainnet_bob())) } + + #[test] + fn hidden() { + let values = &[ + Account::sample_mainnet_bob(), + Account::sample_mainnet_diana(), // This account is hidden + Account::sample_mainnet_erin(), // This account is tombstoned + ]; + let sut = SUT::from_iter(values.clone()); + + assert_eq!(sut.hidden(), SUT::just(Account::sample_mainnet_diana())) + } } diff --git a/crates/sargon/src/profile/logic/account/query_accounts.rs b/crates/sargon/src/profile/logic/account/query_accounts.rs index 046f6d78e..0bdbace8d 100644 --- a/crates/sargon/src/profile/logic/account/query_accounts.rs +++ b/crates/sargon/src/profile/logic/account/query_accounts.rs @@ -7,6 +7,12 @@ impl Profile { self.current_network().map(|n| n.accounts.visible()) } + /// Returns the hidden accounts on the current network, empty if no hidden accounts + /// on the network + pub fn hidden_accounts_on_current_network(&self) -> Result { + self.current_network().map(|n| n.accounts.hidden()) + } + /// Returns **ALL** accounts - including hidden/deleted ones, on **ALL** networks. pub fn accounts_on_all_networks_including_hidden(&self) -> Accounts { self.networks @@ -142,6 +148,15 @@ mod tests { ); } + #[test] + fn hidden_accounts_on_current_network() { + let sut = SUT::sample_other(); + assert_eq!( + sut.hidden_accounts_on_current_network().unwrap(), + Accounts::just(Account::sample_stokenet_olivia()) // nadia is visible + ); + } + #[test] fn test_accounts_for_display_on_current_network() { let sut = SUT::sample(); diff --git a/crates/sargon/src/profile/logic/persona/query_personas.rs b/crates/sargon/src/profile/logic/persona/query_personas.rs index 536b84960..c76ff1211 100644 --- a/crates/sargon/src/profile/logic/persona/query_personas.rs +++ b/crates/sargon/src/profile/logic/persona/query_personas.rs @@ -7,6 +7,10 @@ impl Personas { .filter(|p| !p.is_hidden()) .collect() } + + pub fn hidden(&self) -> Self { + self.clone().into_iter().filter(|p| p.is_hidden()).collect() + } } impl Profile { @@ -33,6 +37,12 @@ impl Profile { self.current_network().map(|n| n.personas.non_hidden()) } + /// Returns the hidden personas on the current network, empty if no hidden personas + /// on the network + pub fn hidden_personas_on_current_network(&self) -> Result { + self.current_network().map(|n| n.personas.hidden()) + } + /// Returns **ALL** personas - including hidden/deleted ones, on **ALL** networks. pub fn personas_on_all_networks_including_hidden(&self) -> Personas { self.networks @@ -77,6 +87,15 @@ mod personas_tests { assert_eq!(sut.non_hidden(), SUT::just(Persona::sample_mainnet())) } + + #[test] + fn hidden() { + let values = + &[Persona::sample_mainnet(), Persona::sample_mainnet_turing()]; + let sut = SUT::from_iter(values.clone()); + + assert_eq!(sut.hidden(), SUT::just(Persona::sample_mainnet_turing())) + } } #[cfg(test)] @@ -104,6 +123,15 @@ mod profile_tests { ); } + #[test] + fn hidden_personas_on_current_network() { + let sut = SUT::sample_other(); + assert_eq!( + sut.hidden_personas_on_current_network().unwrap(), + Personas::just(Persona::sample_stokenet_hermione()) // Leia is visible + ); + } + #[test] fn test_persona_by_address() { let sut = SUT::sample(); diff --git a/crates/sargon/src/profile/logic/profile_network/mod.rs b/crates/sargon/src/profile/logic/profile_network/mod.rs index e45ae4ebc..78d2b0edd 100644 --- a/crates/sargon/src/profile/logic/profile_network/mod.rs +++ b/crates/sargon/src/profile/logic/profile_network/mod.rs @@ -1,5 +1,7 @@ mod profile_network_details; +mod profile_network_entities_linked_to_factor_source; mod profile_network_get_entities; pub use profile_network_details::*; +pub use profile_network_entities_linked_to_factor_source::*; pub use profile_network_get_entities::*; diff --git a/crates/sargon/src/profile/logic/profile_network/profile_network_entities_linked_to_factor_source.rs b/crates/sargon/src/profile/logic/profile_network/profile_network_entities_linked_to_factor_source.rs new file mode 100644 index 000000000..974174be0 --- /dev/null +++ b/crates/sargon/src/profile/logic/profile_network/profile_network_entities_linked_to_factor_source.rs @@ -0,0 +1,105 @@ +use crate::prelude::*; + +impl ProfileNetwork { + /// Returns the entities linked to a given `FactorSource` on the current `ProfileNetwork`. + pub fn entities_linked_to_factor_source( + &self, + factor_source: FactorSource, + integrity: FactorSourceIntegrity, + ) -> Result { + fn filter( + e: &impl HasSecurityState, + factor_source: FactorSource, + ) -> bool { + e.security_state() + .is_linked_to_factor_source(factor_source.clone()) + } + + let accounts = self + .accounts_non_hidden() + .iter() + .filter(|a| filter(a, factor_source.clone())) + .collect::(); + let hidden_accounts = self + .accounts_hidden() + .iter() + .filter(|a| filter(a, factor_source.clone())) + .collect::(); + let personas = self + .personas_non_hidden() + .iter() + .filter(|p| filter(p, factor_source.clone())) + .collect::(); + let hidden_personas = self + .personas_hidden() + .iter() + .filter(|p| filter(p, factor_source.clone())) + .collect::(); + + Ok(EntitiesLinkedToFactorSource::new( + integrity, + accounts, + hidden_accounts, + personas, + hidden_personas, + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ProfileNetwork; + + #[test] + fn entities_linked_to_factor_source() { + // Set up SUT + + // Two visible accounts + let accounts = Accounts::from_iter([ + Account::sample_stokenet_nadia(), + Account::sample_stokenet_paige(), + ]); + // One hidden account + let hidden_accounts = + Accounts::from_iter([Account::sample_stokenet_olivia()]); + // Two visible personas + let personas = Personas::from_iter([ + Persona::sample_stokenet_leia_skywalker(), + Persona::sample_stokenet_connor(), + ]); + // One hidden persona + let hidden_personas = + Personas::from_iter([Persona::sample_stokenet_hermione()]); + + let all_accounts = accounts + .iter() + .chain(hidden_accounts.iter()) + .collect::(); + let all_personas = personas + .iter() + .chain(hidden_personas.iter()) + .collect::(); + let sut = SUT::new( + NetworkID::Stokenet, + all_accounts, + all_personas, + AuthorizedDapps::sample_stokenet(), + ResourcePreferences::sample_stokenet(), + ); + + let result = sut + .entities_linked_to_factor_source( + FactorSource::sample(), + FactorSourceIntegrity::sample(), + ) + .unwrap(); + + assert_eq!(result.accounts, accounts); + assert_eq!(result.hidden_accounts, hidden_accounts); + assert_eq!(result.personas, personas); + assert_eq!(result.hidden_personas, hidden_personas); + } +} diff --git a/crates/sargon/src/profile/logic/profile_network/profile_network_get_entities.rs b/crates/sargon/src/profile/logic/profile_network/profile_network_get_entities.rs index 07b4075b1..bbfc55c58 100644 --- a/crates/sargon/src/profile/logic/profile_network/profile_network_get_entities.rs +++ b/crates/sargon/src/profile/logic/profile_network/profile_network_get_entities.rs @@ -1,12 +1,20 @@ use crate::prelude::*; impl ProfileNetwork { + pub fn accounts_non_hidden(&self) -> Accounts { + self.accounts.visible() + } + + pub fn accounts_hidden(&self) -> Accounts { + self.accounts.hidden() + } + pub fn personas_non_hidden(&self) -> Personas { self.personas.non_hidden() } - pub fn accounts_non_hidden(&self) -> Accounts { - self.accounts.visible() + pub fn personas_hidden(&self) -> Personas { + self.personas.hidden() } pub fn get_entities_erased( 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 index 6a5b15e65..29bc0c978 100644 --- 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 @@ -4,6 +4,13 @@ pub type MatrixOfFactorInstances = AbstractMatrixBuilt; pub trait HasFactorInstances { fn unique_factor_instances(&self) -> IndexSet; + + /// Returns whether the entity is linked to the given factor source. + fn is_linked_to_factor_source(&self, factor_source: FactorSource) -> bool { + self.unique_factor_instances().iter().any(|factor| { + factor.factor_source_id == factor_source.factor_source_id() + }) + } } pub trait HasFactorSourceKindObjectSafe { diff --git a/crates/sargon/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs new file mode 100644 index 000000000..9c412415a --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/entities_linked_to_factor_source.rs @@ -0,0 +1,79 @@ +use crate::prelude::*; + +/// This is the result of checking what entities are linked to a given `FactorSource`. +#[derive(Clone, Debug, PartialEq)] +pub struct EntitiesLinkedToFactorSource { + /// The integrity of the factor source. + pub integrity: FactorSourceIntegrity, + + /// The visible accounts linked to the factor source. + pub accounts: Accounts, + + /// The hidden accounts linked to the factor source. + pub hidden_accounts: Accounts, + + /// The visible personas linked to the factor source. + pub personas: Personas, + + /// The hidden personas linked to the factor source. + pub hidden_personas: Personas, +} + +impl EntitiesLinkedToFactorSource { + pub fn new( + integrity: FactorSourceIntegrity, + accounts: Accounts, + hidden_accounts: Accounts, + personas: Personas, + hidden_personas: Personas, + ) -> Self { + Self { + integrity, + accounts, + hidden_accounts, + personas, + hidden_personas, + } + } +} + +impl HasSampleValues for EntitiesLinkedToFactorSource { + fn sample() -> Self { + Self::new( + FactorSourceIntegrity::sample(), + Accounts::sample(), + Accounts::new(), + Personas::sample(), + Personas::new(), + ) + } + + fn sample_other() -> Self { + Self::new( + FactorSourceIntegrity::sample_other(), + Accounts::sample_other(), + Accounts::new(), + Personas::sample_other(), + Personas::new(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = EntitiesLinkedToFactorSource; + + #[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/v100/entities_linked_to_factor_source/integrity/device.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs new file mode 100644 index 000000000..cc2bd80b5 --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/device.rs @@ -0,0 +1,57 @@ +use crate::prelude::*; + +/// A struct representing the integrity of a device factor source. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DeviceFactorSourceIntegrity { + /// The factor source that is linked to the entities. + pub factor_source: DeviceFactorSource, + + /// Whether the mnemonic of the factor source is present in secure storage. + pub is_mnemonic_present_in_secure_storage: bool, + + /// Whether the mnemonic of the factor source is marked as backed up. + pub is_mnemonic_marked_as_backed_up: bool, +} + +impl DeviceFactorSourceIntegrity { + pub fn new( + factor_source: DeviceFactorSource, + is_mnemonic_present_in_secure_storage: bool, + is_mnemonic_marked_as_backed_up: bool, + ) -> Self { + Self { + factor_source, + is_mnemonic_present_in_secure_storage, + is_mnemonic_marked_as_backed_up, + } + } +} + +impl HasSampleValues for DeviceFactorSourceIntegrity { + fn sample() -> Self { + Self::new(DeviceFactorSource::sample(), true, true) + } + + fn sample_other() -> Self { + Self::new(DeviceFactorSource::sample_other(), false, false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = DeviceFactorSourceIntegrity; + + #[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/v100/entities_linked_to_factor_source/integrity/integrity.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs new file mode 100644 index 000000000..1802d0725 --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/integrity.rs @@ -0,0 +1,63 @@ +use crate::prelude::*; + +/// An enum representing the integrity of a factor source. +#[derive(Clone, Debug, PartialEq)] +pub enum FactorSourceIntegrity { + Device(DeviceFactorSourceIntegrity), + + Ledger(LedgerHardwareWalletFactorSource), +} + +impl HasSampleValues for FactorSourceIntegrity { + fn sample() -> Self { + Self::Device(DeviceFactorSourceIntegrity::sample()) + } + + fn sample_other() -> Self { + Self::Ledger(LedgerHardwareWalletFactorSource::sample()) + } +} + +impl From for FactorSourceIntegrity { + fn from(value: DeviceFactorSourceIntegrity) -> Self { + Self::Device(value) + } +} + +impl From for FactorSourceIntegrity { + fn from(value: LedgerHardwareWalletFactorSource) -> Self { + Self::Ledger(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = FactorSourceIntegrity; + + #[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 from_device() { + assert_eq!(SUT::sample(), DeviceFactorSourceIntegrity::sample().into()) + } + + #[test] + fn from_ledger() { + assert_eq!( + SUT::sample_other(), + LedgerHardwareWalletFactorSource::sample().into() + ) + } +} diff --git a/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs new file mode 100644 index 000000000..4f95cebd8 --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/integrity/mod.rs @@ -0,0 +1,5 @@ +mod device; +mod integrity; + +pub use device::*; +pub use integrity::*; diff --git a/crates/sargon/src/profile/v100/entities_linked_to_factor_source/mod.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/mod.rs new file mode 100644 index 000000000..bc45f7458 --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/mod.rs @@ -0,0 +1,7 @@ +mod entities_linked_to_factor_source; +mod integrity; +mod profile_to_check; + +pub use entities_linked_to_factor_source::*; +pub use integrity::*; +pub use profile_to_check::*; diff --git a/crates/sargon/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs new file mode 100644 index 000000000..d2759a444 --- /dev/null +++ b/crates/sargon/src/profile/v100/entities_linked_to_factor_source/profile_to_check.rs @@ -0,0 +1,42 @@ +use crate::prelude::*; + +/// The Profile to which we want to check the entities linked to a factor source. +#[derive(Clone, Debug, PartialEq)] +#[allow(clippy::large_enum_variant)] +pub enum ProfileToCheck { + /// We should check against the current Profile. + Current, + + /// We should check against a specific Profile. + /// Useful when we are in the Import Mnenmonics flow. + Specific(Profile), +} + +impl HasSampleValues for ProfileToCheck { + fn sample() -> Self { + Self::Current + } + + fn sample_other() -> Self { + Self::Specific(Profile::sample_other()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = ProfileToCheck; + + #[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/v100/entity_security_state/entity_security_state.rs b/crates/sargon/src/profile/v100/entity_security_state/entity_security_state.rs index 240992d17..dc67b1ccd 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 @@ -85,6 +85,19 @@ impl HasSampleValues for EntitySecurityState { } } +impl HasFactorInstances for EntitySecurityState { + fn unique_factor_instances(&self) -> IndexSet { + match self { + EntitySecurityState::Unsecured { value } => { + value.unique_factor_instances() + } + EntitySecurityState::Securified { value } => { + value.unique_factor_instances() + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -314,4 +327,25 @@ mod tests { "#, ); } + + #[test] + fn unique_factor_instances() { + let unsecured = UnsecuredEntityControl::sample(); + let sut = SUT::Unsecured { + value: unsecured.clone(), + }; + assert_eq!( + sut.unique_factor_instances(), + unsecured.unique_factor_instances() + ); + + let secured = SecuredEntityControl::sample(); + let sut = SUT::Securified { + value: secured.clone(), + }; + assert_eq!( + sut.unique_factor_instances(), + secured.unique_factor_instances() + ); + } } diff --git a/crates/sargon/src/profile/v100/mod.rs b/crates/sargon/src/profile/v100/mod.rs index ec44cd059..0c75e3c75 100644 --- a/crates/sargon/src/profile/v100/mod.rs +++ b/crates/sargon/src/profile/v100/mod.rs @@ -1,5 +1,6 @@ mod address; mod app_preferences; +mod entities_linked_to_factor_source; mod entity; mod entity_security_state; mod factors; @@ -12,6 +13,7 @@ mod proto_profile_maybe_with_legacy_p2p_links; pub use address::*; pub use app_preferences::*; +pub use entities_linked_to_factor_source::*; pub use entity::*; pub use entity_security_state::*; pub use factors::*; diff --git a/crates/sargon/src/system/clients/client/secure_storage_client/secure_storage_client.rs b/crates/sargon/src/system/clients/client/secure_storage_client/secure_storage_client.rs index 8b81aa5b0..a1f01c550 100644 --- a/crates/sargon/src/system/clients/client/secure_storage_client/secure_storage_client.rs +++ b/crates/sargon/src/system/clients/client/secure_storage_client/secure_storage_client.rs @@ -205,6 +205,20 @@ impl SecureStorageClient { .await } + /// Checks if a MnemonicWithPassphrase exists for the given `DeviceFactorSource` + pub async fn contains_device_mnemonic( + &self, + device_factor_source: DeviceFactorSource, + ) -> Result { + self.driver + .contains_data_for_key( + SecureStorageKey::DeviceFactorSourceMnemonic { + factor_source_id: device_factor_source.id, + }, + ) + .await + } + pub async fn delete_profile(&self, id: ProfileID) -> Result<()> { warn!("Deleting profile with id: {}", id); self.driver @@ -334,6 +348,34 @@ mod tests { .contains("device")); } + #[actix_rt::test] + async fn contains_device_mnemonic() { + let private = PrivateHierarchicalDeterministicFactorSource::sample(); + let factor_source_id = private.factor_source.id; + let (sut, _) = SecureStorageClient::ephemeral(); + + // It doesn't contain it yet + assert!(!sut + .contains_device_mnemonic(private.factor_source.clone()) + .await + .unwrap()); + + // Save the mnemonic + assert!(sut + .save_mnemonic_with_passphrase( + &private.mnemonic_with_passphrase, + &factor_source_id.clone() + ) + .await + .is_ok()); + + // Assert it contains it now + assert!(sut + .contains_device_mnemonic(private.factor_source) + .await + .unwrap()); + } + #[actix_rt::test] async fn delete_mnemonic() { // ARRANGE diff --git a/crates/sargon/src/system/clients/client/unsafe_storage_client/unsafe_storage_client.rs b/crates/sargon/src/system/clients/client/unsafe_storage_client/unsafe_storage_client.rs index 1b95e1051..c0c414a65 100644 --- a/crates/sargon/src/system/clients/client/unsafe_storage_client/unsafe_storage_client.rs +++ b/crates/sargon/src/system/clients/client/unsafe_storage_client/unsafe_storage_client.rs @@ -11,3 +11,179 @@ impl UnsafeStorageClient { Self { driver } } } + +impl UnsafeStorageClient { + //====== + // Save T + //====== + pub async fn save(&self, key: UnsafeStorageKey, value: &T) -> Result<()> + where + T: serde::Serialize, + { + let json = serde_json::to_vec(value) + .map_err(|_| CommonError::FailedToSerializeToJSON)?; + self.driver + .save_data(key, BagOfBytes::from(json)) + // tarpaulin will incorrectly flag next line is missed + .await + } + + //====== + // Load T + //====== + /// Loads bytes from UnsafeStorageDriver and deserializes them into `T`. + /// + /// Returns `Ok(None)` if no bytes were found, returns Err if failed + /// to load bytes or failed to deserialize the JSON into a `T`. + pub async fn load(&self, key: UnsafeStorageKey) -> Result> + where + T: for<'a> serde::Deserialize<'a>, + { + self.driver.load_data(key).await.and_then(|o| match o { + None => Ok(None), + Some(j) => serde_json::from_slice(j.as_slice()) + .map_failed_to_deserialize_bytes::(j.as_slice()), + }) + } + + /// Loads bytes from UnsafeStorageDriver and deserializes them into `T`. + /// + /// Returns Err if failed to load bytes or failed to deserialize the JSON into a `T`, + /// unlike `load` this method returns `default` if `None` bytes were found. + pub async fn load_unwrap_or( + &self, + key: UnsafeStorageKey, + default: T, + ) -> T + where + T: for<'a> serde::Deserialize<'a> + Clone, + { + self.load(key) + .await + .map(|o| o.unwrap_or(default.clone())) + .unwrap_or(default) + } + + //====== + // Backup up mnemonics + //====== + pub async fn check_if_mnemonic_is_backed_up( + &self, + factor_source: DeviceFactorSource, + ) -> Result { + let result: Vec = self + .load_unwrap_or( + UnsafeStorageKey::FactorSourceUserHasWrittenDown, + Vec::new(), + ) + .await; + Ok(result.contains(&factor_source.id)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_sut() -> UnsafeStorageClient { + let driver = EphemeralUnsafeStorage::new(); + UnsafeStorageClient::new(driver) + } + + #[actix_rt::test] + async fn load_ok_when_none() { + let sut = make_sut(); + assert_eq!( + sut.load::( + UnsafeStorageKey::FactorSourceUserHasWrittenDown + ) + .await, + Ok(None) + ); + } + + #[actix_rt::test] + async fn load_successful() { + let sut = make_sut(); + + let value = String::sample(); + assert!(sut + .save(UnsafeStorageKey::FactorSourceUserHasWrittenDown, &value) + .await + .is_ok()); + assert_eq!( + sut.load::( + UnsafeStorageKey::FactorSourceUserHasWrittenDown + ) + .await, + Ok(Some(value)) + ); + } + + #[actix_rt::test] + async fn load_unwrap_or_some_default_not_used() { + let sut = make_sut(); + + let value = String::sample(); + let default = String::sample_other(); + assert!(sut + .save(UnsafeStorageKey::FactorSourceUserHasWrittenDown, &value) + .await + .is_ok()); + assert_eq!( + sut.load_unwrap_or::( + UnsafeStorageKey::FactorSourceUserHasWrittenDown, + default + ) + .await, + value + ); + } + + #[actix_rt::test] + async fn load_unwrap_or_none_default_is_used() { + let sut = make_sut(); + + assert_eq!( + sut.load_unwrap_or::( + UnsafeStorageKey::FactorSourceUserHasWrittenDown, + String::sample_other() + ) + .await, + String::sample_other() + ); + } + + #[actix_rt::test] + async fn check_if_mnemonic_is_backed_up() { + let sut = make_sut(); + let factor_source = DeviceFactorSource::sample(); + + // Check it isn't initially backed up + assert!(!sut + .check_if_mnemonic_is_backed_up(factor_source.clone()) + .await + .unwrap()); + + // Backup the mnemonic + assert!(sut + .save( + UnsafeStorageKey::FactorSourceUserHasWrittenDown, + &vec![factor_source.id] + ) + .await + .is_ok()); + + // Check it is now backed up + assert!(sut + .check_if_mnemonic_is_backed_up(factor_source.clone()) + .await + .unwrap()); + + // Check another factor source isn't backed up + assert!(!sut + .check_if_mnemonic_is_backed_up(DeviceFactorSource::sample_other()) + .await + .unwrap()); + } +} diff --git a/crates/sargon/src/system/drivers/drivers.rs b/crates/sargon/src/system/drivers/drivers.rs index 536fdd6f7..7e3ddd253 100644 --- a/crates/sargon/src/system/drivers/drivers.rs +++ b/crates/sargon/src/system/drivers/drivers.rs @@ -94,6 +94,23 @@ impl Drivers { ) } + pub fn with_storages( + secure_storage: Arc, + unsafe_storage: Arc, + ) -> Arc { + Drivers::new( + RustNetworkingDriver::new(), + secure_storage, + RustEntropyDriver::new(), + RustHostInfoDriver::new(), + RustLoggingDriver::new(), + RustEventBusDriver::new(), + Self::file_system(), + unsafe_storage, + RustProfileStateChangeDriver::new(), + ) + } + pub fn with_entropy_provider( entropy_provider: Arc, ) -> Arc { diff --git a/crates/sargon/src/system/drivers/secure_storage_driver/secure_storage_driver.rs b/crates/sargon/src/system/drivers/secure_storage_driver/secure_storage_driver.rs index d9546c883..acc528a60 100644 --- a/crates/sargon/src/system/drivers/secure_storage_driver/secure_storage_driver.rs +++ b/crates/sargon/src/system/drivers/secure_storage_driver/secure_storage_driver.rs @@ -14,4 +14,9 @@ pub trait SecureStorageDriver: Send + Sync + std::fmt::Debug { ) -> Result<()>; async fn delete_data_for_key(&self, key: SecureStorageKey) -> Result<()>; + + async fn contains_data_for_key( + &self, + key: SecureStorageKey, + ) -> Result; } diff --git a/crates/sargon/src/system/drivers/secure_storage_driver/support/test/ephemeral_secure_storage.rs b/crates/sargon/src/system/drivers/secure_storage_driver/support/test/ephemeral_secure_storage.rs index 3d1649140..c457e9664 100644 --- a/crates/sargon/src/system/drivers/secure_storage_driver/support/test/ephemeral_secure_storage.rs +++ b/crates/sargon/src/system/drivers/secure_storage_driver/support/test/ephemeral_secure_storage.rs @@ -53,4 +53,14 @@ impl SecureStorageDriver for EphemeralSecureStorage { storage.remove_entry(&key); Ok(()) } + + async fn contains_data_for_key( + &self, + key: SecureStorageKey, + ) -> Result { + self.storage + .try_read() + .map_err(|_| CommonError::SecureStorageReadError) + .map(|s| s.contains_key(&key)) + } } diff --git a/crates/sargon/src/system/drivers/secure_storage_driver/support/test/fail_secure_storage.rs b/crates/sargon/src/system/drivers/secure_storage_driver/support/test/fail_secure_storage.rs index ddb1470b5..003738e41 100644 --- a/crates/sargon/src/system/drivers/secure_storage_driver/support/test/fail_secure_storage.rs +++ b/crates/sargon/src/system/drivers/secure_storage_driver/support/test/fail_secure_storage.rs @@ -25,4 +25,11 @@ impl SecureStorageDriver for AlwaysFailSecureStorage { async fn delete_data_for_key(&self, _key: SecureStorageKey) -> Result<()> { panic!("AlwaysFailStorage does not implement `delete_data_for_key"); } + + async fn contains_data_for_key( + &self, + _key: SecureStorageKey, + ) -> Result { + panic!("AlwaysFailStorage does not implement `contains_data_for_key"); + } } diff --git a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs b/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs index fd18ffa85..a10d68eed 100644 --- a/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs +++ b/crates/sargon/src/system/drivers/unsafe_storage_driver/support/test/mod.rs @@ -1,5 +1,4 @@ #[cfg(test)] mod ephemeral_unsafe_storage; - #[cfg(test)] pub use ephemeral_unsafe_storage::*; diff --git a/crates/sargon/src/system/sargon_os/mod.rs b/crates/sargon/src/system/sargon_os/mod.rs index 6fd70fdec..70aa35a50 100644 --- a/crates/sargon/src/system/sargon_os/mod.rs +++ b/crates/sargon/src/system/sargon_os/mod.rs @@ -3,6 +3,7 @@ mod pre_authorization; mod profile_state_holder; mod sargon_os; mod sargon_os_accounts; +mod sargon_os_entities_linked_to_factor_source; mod sargon_os_factors; mod sargon_os_gateway; mod sargon_os_personas; @@ -18,6 +19,7 @@ pub use pre_authorization::*; pub use profile_state_holder::*; pub use sargon_os::*; pub use sargon_os_accounts::*; +pub use sargon_os_entities_linked_to_factor_source::*; pub use sargon_os_factors::*; pub use sargon_os_gateway::*; pub use sargon_os_personas::*; diff --git a/crates/sargon/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs b/crates/sargon/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs new file mode 100644 index 000000000..2563786d2 --- /dev/null +++ b/crates/sargon/src/system/sargon_os/sargon_os_entities_linked_to_factor_source.rs @@ -0,0 +1,317 @@ +use crate::prelude::*; + +impl SargonOS { + /// Returns the entities linked to a given `FactorSource`, either on the current `Profile` or a specific one. + pub async fn entities_linked_to_factor_source( + &self, + factor_source: FactorSource, + profile_to_check: ProfileToCheck, + ) -> Result { + let integrity = self.integrity(factor_source.clone()).await?; + match profile_to_check { + ProfileToCheck::Current => self + .profile()? + .current_network()? + .entities_linked_to_factor_source(factor_source, integrity), + ProfileToCheck::Specific(specific_profile) => { + let profile_network = specific_profile + .networks + .get_id(NetworkID::Mainnet) + .ok_or(CommonError::Unknown)?; + profile_network + .entities_linked_to_factor_source(factor_source, integrity) + } + } + } + + async fn integrity( + &self, + factor_source: FactorSource, + ) -> Result { + match factor_source { + FactorSource::Device { value } => { + self.device_integrity(value).await + } + FactorSource::Ledger { value } => Ok(value.into()), + _ => Err(CommonError::Unknown), + } + } + + async fn device_integrity( + &self, + device_factor_source: DeviceFactorSource, + ) -> Result { + let is_mnemonic_present_in_secure_storage = self + .clients + .secure_storage + .contains_device_mnemonic(device_factor_source.clone()) + .await?; + let is_mnemonic_marked_as_backed_up = self + .clients + .unsafe_storage + .check_if_mnemonic_is_backed_up(device_factor_source.clone()) + .await?; + let result = DeviceFactorSourceIntegrity::new( + device_factor_source, + is_mnemonic_present_in_secure_storage, + is_mnemonic_marked_as_backed_up, + ); + Ok(result.into()) + } +} + +#[cfg(test)] +#[allow(non_snake_case)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SargonOS; + + #[actix_rt::test] + async fn current_profile__device_factor_source_present_in_secure_storage_and_backed_up( + ) { + // Verify the integrity and entities when the device mnemonic is present in the secure storage and marked as backed up. + let os = boot_with_entities(true, true).await; + let factor_source = FactorSource::sample_device(); + + let result = os + .entities_linked_to_factor_source( + factor_source, + ProfileToCheck::Current, + ) + .await + .unwrap(); + verify_entities(result.clone()); + verify_device_integrity(result.integrity, true, true); + } + + #[actix_rt::test] + async fn current_profile__device_factor_source_present_in_secure_storage_but_not_backed_up( + ) { + // Verify the integrity and entities when the device mnemonic is present in secure storage but not marked as backed up. + let os = boot_with_entities(true, false).await; + let factor_source = FactorSource::sample_device(); + + let result = os + .entities_linked_to_factor_source( + factor_source, + ProfileToCheck::Current, + ) + .await + .unwrap(); + verify_entities(result.clone()); + verify_device_integrity(result.integrity, true, false); + } + + #[actix_rt::test] + async fn current_profile__device_factor_source_missing_in_secure_storage_and_not_backed_up( + ) { + // Verify the integrity and entities when the device mnemonic is not present in secure storage and not marked as backed up. + let os = boot_with_entities(false, false).await; + let factor_source = FactorSource::sample_device(); + + let result = os + .entities_linked_to_factor_source( + factor_source, + ProfileToCheck::Current, + ) + .await + .unwrap(); + verify_entities(result.clone()); + verify_device_integrity(result.integrity, false, false); + } + + #[actix_rt::test] + async fn current_profile__ledger_factor_source() { + // Verify the integrity and entities for ledger factor source. + let os = boot_with_entities(false, false).await; + let factor_source = FactorSource::sample_ledger(); + + let result = os + .entities_linked_to_factor_source( + factor_source.clone(), + ProfileToCheck::Current, + ) + .await + .unwrap(); + assert!(result.accounts.is_empty()); + assert!(result.hidden_accounts.is_empty()); + assert!(result.personas.is_empty()); + assert!(result.hidden_personas.is_empty()); + match result.integrity { + FactorSourceIntegrity::Ledger(integrity) => { + assert_eq!(integrity, *factor_source.as_ledger().unwrap()); + } + _ => panic!("Expected Ledger integrity"), + } + } + + #[actix_rt::test] + async fn specific_profile__checks_on_mainnet() { + // Verify the entities when checking for a specific Profile (which will check on Mainnet, regardless of the current network set on Profile) + let mut profile = Profile::sample(); + profile + .app_preferences + .gateways + .change_current(Gateway::stokenet()); + let os = boot_with_entities(true, true).await; + let factor_source = FactorSource::sample_device(); + + let result = os + .entities_linked_to_factor_source( + factor_source, + ProfileToCheck::Specific(profile), + ) + .await + .unwrap(); + verify_device_integrity(result.integrity, true, true); + + assert_eq!(result.accounts, Accounts::sample_mainnet()); // Alice and Bob, which are both visible + assert!(result.hidden_accounts.is_empty()); + assert_eq!(result.personas, Personas::sample_mainnet()); // Satoshi and Batman, which are both visible + assert!(result.hidden_personas.is_empty()); + } + + #[actix_rt::test] + async fn specific_profile__mainnet_missing() { + // Test the failure case when checking entities for a specific Profile that doesn't have Mainnet in its networks + let profile = Profile::sample_other(); + let os = boot_with_entities(true, true).await; + let factor_source = FactorSource::sample_device(); + + let result = os + .entities_linked_to_factor_source( + factor_source, + ProfileToCheck::Specific(profile), + ) + .await + .expect_err("Expected an error"); + assert_eq!(result, CommonError::Unknown); + } + + /// Verifies the integrity corresponds to a DeviceFactorSourceIntegrity with the expected values + fn verify_device_integrity( + result: FactorSourceIntegrity, + is_mnemonic_present_in_secure_storage: bool, + is_mnemonic_marked_as_backed_up: bool, + ) { + match result { + FactorSourceIntegrity::Device(integrity) => { + assert_eq!( + integrity.is_mnemonic_present_in_secure_storage, + is_mnemonic_present_in_secure_storage + ); + assert_eq!( + integrity.is_mnemonic_marked_as_backed_up, + is_mnemonic_marked_as_backed_up + ); + } + _ => panic!("Expected Device integrity"), + } + } + + /// Verifies the entities linked to a factor source are the expected ones. + fn verify_entities(result: EntitiesLinkedToFactorSource) { + assert_eq!( + result.accounts, + Accounts::just(Account::sample_stokenet_nadia()) + ); + assert_eq!( + result.hidden_accounts, + Accounts::just(Account::sample_stokenet_olivia()) + ); + assert_eq!( + result.personas, + Personas::just(Persona::sample_stokenet_leia_skywalker()) + ); + assert!(result.hidden_personas.is_empty()); + } + + /// Will boot SargonOS with a profile that has the following entities on Stokenet (its current network): + /// - 1 visible Account (sample_stokenet_nadia) + /// - 1 hidden Account (sample_stokenet_olivia) + /// - 1 visible Persona (sample_stokenet_leia_skywalker) + /// And the corresponding mocked secure/unsafe storages. + async fn boot_with_entities( + device_mnemonic_in_secure_storage: bool, + device_mnemonic_backed_up: bool, + ) -> Arc { + let secure_storage = + build_secure_storage(device_mnemonic_in_secure_storage).await; + let unsafe_storage = + build_unsafe_storage(device_mnemonic_backed_up).await; + let drivers = Drivers::with_storages(secure_storage, unsafe_storage); + let bios = Bios::new(drivers); + let clients = Clients::new(bios); + let interactors = Interactors::new_from_clients(&clients); + + let os = + SUT::boot_with_clients_and_interactor(clients, interactors).await; + + let (mut profile, _) = + os.create_new_profile_with_bdfs(None).await.unwrap(); + + let new_network = ProfileNetwork::new( + NetworkID::Stokenet, + Accounts::from_iter([ + Account::sample_stokenet_nadia(), + Account::sample_stokenet_olivia(), + ]), + Personas::just(Persona::sample_stokenet_leia_skywalker()), + AuthorizedDapps::new(), + ResourcePreferences::new(), + ); + + profile.networks.append(new_network); + profile + .app_preferences + .gateways + .change_current(Gateway::stokenet()); + + os.with_timeout(|x| x.set_profile(profile.clone())) + .await + .unwrap(); + + os + } + + fn device_secure_key() -> SecureStorageKey { + SecureStorageKey::DeviceFactorSourceMnemonic { + factor_source_id: FactorSource::sample_device().id_from_hash(), + } + } + + async fn build_secure_storage( + device_mnemonic_in_secure_storage: bool, + ) -> Arc { + let secure_storage = EphemeralSecureStorage::new(); + if device_mnemonic_in_secure_storage { + secure_storage + .save_data(device_secure_key(), BagOfBytes::from(vec![0x01])) + .await + .unwrap(); + } + secure_storage + } + + async fn build_unsafe_storage( + device_mnemonic_backed_up: bool, + ) -> Arc { + let unsafe_storage = EphemeralUnsafeStorage::new(); + let backed_up = if device_mnemonic_backed_up { + vec![FactorSource::sample_device().id_from_hash()] + } else { + vec![] + }; + let json = serde_json::to_vec(&backed_up).unwrap(); + unsafe_storage + .save_data( + UnsafeStorageKey::FactorSourceUserHasWrittenDown, + BagOfBytes::from(json), + ) + .await + .unwrap(); + unsafe_storage + } +} diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/driver/AndroidStorageDriver.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/driver/AndroidStorageDriver.kt index 5daf52400..075cfc895 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/driver/AndroidStorageDriver.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/driver/AndroidStorageDriver.kt @@ -2,6 +2,7 @@ package com.radixdlt.sargon.os.driver import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey import com.radixdlt.sargon.BagOfBytes import com.radixdlt.sargon.CommonException import com.radixdlt.sargon.SecureStorageDriver @@ -13,6 +14,8 @@ import com.radixdlt.sargon.os.storage.key.ByteArrayKeyMapping import com.radixdlt.sargon.os.storage.key.DeviceFactorSourceMnemonicKeyMapping import com.radixdlt.sargon.os.storage.key.HostIdKeyMapping import com.radixdlt.sargon.os.storage.key.ProfileSnapshotKeyMapping +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map internal class AndroidStorageDriver( private val biometricAuthorizationDriver: BiometricAuthorizationDriver, @@ -39,6 +42,12 @@ internal class AndroidStorageDriver( .reportSecureStorageWriteFailure(key = key) } + override suspend fun containsDataForKey(key: SecureStorageKey): Boolean = key + .mapping() + .getOrNull() + ?.keyExist() + ?: false + override suspend fun loadData(key: UnsafeStorageKey): BagOfBytes? = key .mapping() .then { it.read() } diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/StorageUtils.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/StorageUtils.kt index 6c7ba1f86..267558c21 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/StorageUtils.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/StorageUtils.kt @@ -5,11 +5,13 @@ import androidx.datastore.core.IOException import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.core.preferencesOf import com.radixdlt.sargon.annotation.KoverIgnore import com.radixdlt.sargon.extensions.then import com.radixdlt.sargon.extensions.toUnit import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.retryWhen @@ -66,6 +68,10 @@ suspend fun DataStore.remove(key: Preferences.Key) = runCatc } }.toUnit() +suspend fun DataStore.keyExist(key: Preferences.Key) = this.data.map { preference -> + preference.contains(key) +}.first() + @KoverIgnore internal fun Flow.catchIOException() = catch { exception -> if (exception is IOException) { diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ByteArrayKeyMapping.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ByteArrayKeyMapping.kt index f77a443eb..40f8b2ae5 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ByteArrayKeyMapping.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ByteArrayKeyMapping.kt @@ -10,6 +10,7 @@ import com.radixdlt.sargon.extensions.identifier import com.radixdlt.sargon.extensions.toBagOfBytes import com.radixdlt.sargon.extensions.toByteArray import com.radixdlt.sargon.os.storage.KeystoreAccessRequest +import com.radixdlt.sargon.os.storage.keyExist import com.radixdlt.sargon.os.storage.read import com.radixdlt.sargon.os.storage.remove import com.radixdlt.sargon.os.storage.write @@ -75,6 +76,11 @@ internal class ByteArrayKeyMapping private constructor( is ByteArrayKeyMappingInput.Unsecure -> input.storage.remove(preferencesKey) } + override suspend fun keyExist(): Boolean = when (input) { + is ByteArrayKeyMappingInput.Secure -> input.storage.keyExist(preferencesKey) + is ByteArrayKeyMappingInput.Unsecure -> input.storage.keyExist(preferencesKey) + } + private sealed interface ByteArrayKeyMappingInput { val storage: DataStore diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DatastoreKeyMapping.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DatastoreKeyMapping.kt index 646de03bb..12dc6bb24 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DatastoreKeyMapping.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DatastoreKeyMapping.kt @@ -10,4 +10,5 @@ internal interface DatastoreKeyMapping { suspend fun remove(): Result + suspend fun keyExist(): Boolean } \ No newline at end of file diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DeviceFactorSourceMnemonicKeyMapping.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DeviceFactorSourceMnemonicKeyMapping.kt index 4a16e5c82..eec27478c 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DeviceFactorSourceMnemonicKeyMapping.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/DeviceFactorSourceMnemonicKeyMapping.kt @@ -18,10 +18,12 @@ import com.radixdlt.sargon.os.storage.KeystoreAccessRequest import com.radixdlt.sargon.os.storage.read import com.radixdlt.sargon.os.storage.remove import com.radixdlt.sargon.os.storage.write +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import timber.log.Timber internal class DeviceFactorSourceMnemonicKeyMapping( - private val key: SecureStorageKey.DeviceFactorSourceMnemonic, + key: SecureStorageKey.DeviceFactorSourceMnemonic, private val encryptedStorage: DataStore, private val biometricAuthorizationDriver: BiometricAuthorizationDriver ): DatastoreKeyMapping { @@ -55,4 +57,10 @@ internal class DeviceFactorSourceMnemonicKeyMapping( } override suspend fun remove(): Result = encryptedStorage.remove(key = preferencesKey) + + override suspend fun keyExist(): Boolean { + return encryptedStorage.data.map { preference -> + preference.contains(preferencesKey) + }.first() + } } \ No newline at end of file diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/HostIdKeyMapping.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/HostIdKeyMapping.kt index b99991448..f5a73b54d 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/HostIdKeyMapping.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/HostIdKeyMapping.kt @@ -11,6 +11,7 @@ import com.radixdlt.sargon.Uuid import com.radixdlt.sargon.extensions.then import com.radixdlt.sargon.hostIdToJsonBytes import com.radixdlt.sargon.newHostIdFromJsonBytes +import com.radixdlt.sargon.os.storage.keyExist import com.radixdlt.sargon.os.storage.read import com.radixdlt.sargon.os.storage.remove import com.radixdlt.sargon.os.storage.write @@ -52,6 +53,8 @@ internal class HostIdKeyMapping( override suspend fun remove(): Result = deviceStorage.remove(preferencesKey) + override suspend fun keyExist(): Boolean = deviceStorage.keyExist(preferencesKey) + companion object { private const val PREFERENCES_KEY = "key_device_info" } diff --git a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ProfileSnapshotKeyMapping.kt b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ProfileSnapshotKeyMapping.kt index 21bc7f685..13094cdd8 100644 --- a/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ProfileSnapshotKeyMapping.kt +++ b/jvm/sargon-android/src/main/java/com/radixdlt/sargon/os/storage/key/ProfileSnapshotKeyMapping.kt @@ -9,6 +9,7 @@ import com.radixdlt.sargon.extensions.bagOfBytes import com.radixdlt.sargon.extensions.string import com.radixdlt.sargon.extensions.then import com.radixdlt.sargon.os.storage.KeystoreAccessRequest +import com.radixdlt.sargon.os.storage.keyExist import com.radixdlt.sargon.os.storage.read import com.radixdlt.sargon.os.storage.remove import com.radixdlt.sargon.os.storage.write @@ -53,6 +54,9 @@ internal class ProfileSnapshotKeyMapping( override suspend fun remove(): Result = encryptedStorage.remove(preferenceKey) + override suspend fun keyExist(): Boolean = encryptedStorage.keyExist(preferenceKey) + + companion object { private const val KEY = "profile_preferences_key" private const val RETRY_ON_IO_EXCEPTION_COUNT = 3L diff --git a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/driver/Fakes.kt b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/driver/Fakes.kt index 2db49de87..a7911b198 100644 --- a/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/driver/Fakes.kt +++ b/jvm/sargon-android/src/test/java/com/radixdlt/sargon/os/driver/Fakes.kt @@ -27,6 +27,10 @@ class FakeSecureStorageDriver: SecureStorageDriver { override suspend fun deleteDataForKey(key: SecureStorageKey) { storage.remove(key.identifier) } + + override suspend fun containsDataForKey(key: SecureStorageKey): Boolean { + return storage.containsKey(key.identifier) + } } class FakeUnsafeStorageDriver: UnsafeStorageDriver {