diff --git a/Cargo.lock b/Cargo.lock index c5d80a175..677b27dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,7 +2772,7 @@ dependencies = [ [[package]] name = "sargon" -version = "1.1.74" +version = "1.1.75" dependencies = [ "actix-rt", "aes-gcm", @@ -2827,7 +2827,7 @@ dependencies = [ [[package]] name = "sargon-uniffi" -version = "1.1.74" +version = "1.1.75" dependencies = [ "actix-rt", "assert-json-diff", diff --git a/apple/Sources/Sargon/Drivers/FileSystem/FileSystemDriver+Data+ContentsOf+URL.swift b/apple/Sources/Sargon/Drivers/FileSystem/FileSystemDriver+Data+ContentsOf+URL.swift index 8d380bb38..c4dd11d74 100644 --- a/apple/Sources/Sargon/Drivers/FileSystem/FileSystemDriver+Data+ContentsOf+URL.swift +++ b/apple/Sources/Sargon/Drivers/FileSystem/FileSystemDriver+Data+ContentsOf+URL.swift @@ -51,7 +51,7 @@ extension FileSystem { extension FileSystem { private static func appDirPathNotNecessarilyExisting(fileManager: FileManager) throws -> String { #if os(iOS) - return try fileManager.urls( + fileManager.urls( for: .cachesDirectory, in: .userDomainMask ).first!.path() diff --git a/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblem+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblem+Wrap+Functions.swift new file mode 100644 index 000000000..973d2cfac --- /dev/null +++ b/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblem+Wrap+Functions.swift @@ -0,0 +1,15 @@ +import Foundation +import SargonUniFFI + +extension SecurityProblem { + public var kind: SecurityProblemKind { + securityProblemKind(value: self) + } +} + +// MARK: - SecurityProblem + Identifiable +extension SecurityProblem: Identifiable { + public var id: UInt64 { + securityProblemId(value: self) + } +} diff --git a/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblemKind+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblemKind+Wrap+Functions.swift new file mode 100644 index 000000000..e89774e00 --- /dev/null +++ b/apple/Sources/Sargon/Extensions/Methods/SecurityCenter/SecurityProblemKind+Wrap+Functions.swift @@ -0,0 +1,8 @@ +import Foundation +import SargonUniFFI + +extension SecurityProblemKind: CaseIterable { + public static var allCases: [SecurityProblemKind] { + [.configurationBackup, .securityFactors] + } +} diff --git a/apple/Tests/TestCases/SecurityCenter/SecurityProblemTests.swift b/apple/Tests/TestCases/SecurityCenter/SecurityProblemTests.swift new file mode 100644 index 000000000..e699446ba --- /dev/null +++ b/apple/Tests/TestCases/SecurityCenter/SecurityProblemTests.swift @@ -0,0 +1,41 @@ +import Foundation +import SargonUniFFI +import XCTest + +final class SecurityProblemTests: TestCase { + typealias SUT = SecurityProblem + + func testKind() throws { + var sut = SUT.problem3(addresses: .init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: [])) + XCTAssertEqual(sut.kind, .securityFactors) + + sut = SUT.problem5 + XCTAssertEqual(sut.kind, .configurationBackup) + + sut = SUT.problem6 + XCTAssertEqual(sut.kind, .configurationBackup) + + sut = SUT.problem7 + XCTAssertEqual(sut.kind, .configurationBackup) + + sut = .problem9(addresses: .init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: [])) + XCTAssertEqual(sut.kind, .securityFactors) + } + + func testId() throws { + var sut = SUT.problem3(addresses: .init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: [])) + XCTAssertEqual(sut.id, 3) + + sut = SUT.problem5 + XCTAssertEqual(sut.id, 5) + + sut = SUT.problem6 + XCTAssertEqual(sut.id, 6) + + sut = SUT.problem7 + XCTAssertEqual(sut.id, 7) + + sut = .problem9(addresses: .init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: [])) + XCTAssertEqual(sut.id, 9) + } +} diff --git a/crates/sargon-uniffi/Cargo.toml b/crates/sargon-uniffi/Cargo.toml index d164a90e1..6923d3397 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.74" +version = "1.1.75" edition = "2021" build = "build.rs" diff --git a/crates/sargon-uniffi/src/lib.rs b/crates/sargon-uniffi/src/lib.rs index 2621cb0fe..9d8cbc3c5 100644 --- a/crates/sargon-uniffi/src/lib.rs +++ b/crates/sargon-uniffi/src/lib.rs @@ -10,6 +10,7 @@ mod home_cards; mod keys_collector; mod profile; mod radix_connect; +mod security_center; mod signing; mod system; mod types; @@ -23,6 +24,7 @@ pub mod prelude { pub use crate::keys_collector::*; pub use crate::profile::*; pub use crate::radix_connect::*; + pub use crate::security_center::*; pub use crate::signing::*; pub use crate::system::*; pub use crate::types::*; diff --git a/crates/sargon-uniffi/src/security_center/mod.rs b/crates/sargon-uniffi/src/security_center/mod.rs new file mode 100644 index 000000000..4fb296a66 --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/mod.rs @@ -0,0 +1,7 @@ +mod security_problem; +mod security_problem_kind; +mod support; + +pub use security_problem::*; +pub use security_problem_kind::*; +pub use support::*; diff --git a/crates/sargon-uniffi/src/security_center/security_problem.rs b/crates/sargon-uniffi/src/security_center/security_problem.rs new file mode 100644 index 000000000..54b85a17e --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/security_problem.rs @@ -0,0 +1,46 @@ +use crate::prelude::*; +use sargon::SecurityProblem as InternalSecurityProblem; + +#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Enum)] +/// An enum describing each potential Security Problem the Wallet can encounter. +/// +/// See [the Confluence doc for details][doc]. +/// +/// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3392569357/Security-related+Problem+States+in+the+Wallet +pub enum SecurityProblem { + /// The given addresses of `accounts` and `personas` are unrecoverable if the user loses their phone, since their corresponding seed phrase has not been written down. + /// NOTE: This definition differs from the one at Confluence since we don't have shields implemented yet. + Problem3 { + addresses: AddressesOfEntitiesInBadState, + }, + + /// Wallet backups to the cloud aren’t working (wallet tried to do a backup, and it didn’t work within, say, 5 minutes.) + /// This means that currently all accounts and personas are at risk of being practically unrecoverable if the user loses their phone. + /// Also they would lose all of their other non-security wallet settings and data. + Problem5, + + /// Cloud backups are turned off and user has never done a manual file export. This means that currently all accounts and personas are at risk of + /// being practically unrecoverable if the user loses their phone. Also, they would lose all of their other non-security wallet settings and data. + Problem6, + + /// Cloud backups are turned off and user previously did a manual file export, but has made a change and haven’t yet re-exported a file backup that + /// includes that change. This means that any changes made will be lost if the user loses their phone - including control of new accounts/personas they’ve + /// created, as well as changed settings or changed/added data. + Problem7, + + /// User has gotten a new phone (and restored their wallet from backup) and the wallet sees that there are accounts without shields using a phone key, + /// meaning they can only be recovered with the seed phrase. (See problem 2) This would also be the state if a user disabled their PIN (and reenabled it), clearing phone keys. + Problem9 { + addresses: AddressesOfEntitiesInBadState, + }, +} + +#[uniffi::export] +pub fn security_problem_kind(value: &SecurityProblem) -> SecurityProblemKind { + value.into_internal().kind().into() +} + +#[uniffi::export] +pub fn security_problem_id(value: &SecurityProblem) -> u64 { + value.into_internal().id() +} diff --git a/crates/sargon-uniffi/src/security_center/security_problem_kind.rs b/crates/sargon-uniffi/src/security_center/security_problem_kind.rs new file mode 100644 index 000000000..a0f591558 --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/security_problem_kind.rs @@ -0,0 +1,10 @@ +use crate::prelude::*; +use sargon::SecurityProblemKind as InternalSecurityProblemKind; + +#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Enum)] +/// An enum describing the different types of Security Problems the Wallet can encounter. +pub enum SecurityProblemKind { + SecurityFactors, + + ConfigurationBackup, +} diff --git a/crates/sargon-uniffi/src/security_center/support/addresses_entities_bad_state.rs b/crates/sargon-uniffi/src/security_center/support/addresses_entities_bad_state.rs new file mode 100644 index 000000000..d7e2c0804 --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/support/addresses_entities_bad_state.rs @@ -0,0 +1,11 @@ +use crate::prelude::*; +use sargon::AddressesOfEntitiesInBadState as InternalAddressesOfEntitiesInBadState; + +/// A struct that represents the addresses of entities in a bad state. +#[derive(Clone, PartialEq, Eq, Hash, InternalConversion, uniffi::Record)] +pub struct AddressesOfEntitiesInBadState { + pub accounts: Vec, + pub hidden_accounts: Vec, + pub personas: Vec, + pub hidden_personas: Vec, +} diff --git a/crates/sargon-uniffi/src/security_center/support/backup_result.rs b/crates/sargon-uniffi/src/security_center/support/backup_result.rs new file mode 100644 index 000000000..16f36133e --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/support/backup_result.rs @@ -0,0 +1,39 @@ +use crate::prelude::*; +use sargon::BackupResult as InternalBackupResult; +use sargon::IsBackupResultCurrent; +use sargon::IsBackupResultFailed; + +/// A struct that represents the result of a given backup. +/// +/// Reference for iOS: it is a combination of `BackupStatus` and `BackupResult` (all in one). +#[derive(Clone, PartialEq, Eq, uniffi::Record)] +pub struct BackupResult { + /// The identifier of the backup. + pub save_identifier: String, + + /// Whether this backup matches the one on Profile. + pub is_current: bool, + + /// Whether this backup has failed. + pub is_failed: bool, +} + +impl From for BackupResult { + fn from(internal: InternalBackupResult) -> Self { + Self { + save_identifier: internal.save_identifier, + is_current: internal.is_current.0, + is_failed: internal.is_failed.0, + } + } +} + +impl From for InternalBackupResult { + fn from(backup_result: BackupResult) -> Self { + InternalBackupResult { + save_identifier: backup_result.save_identifier, + is_current: IsBackupResultCurrent(backup_result.is_current), + is_failed: IsBackupResultFailed(backup_result.is_failed), + } + } +} diff --git a/crates/sargon-uniffi/src/security_center/support/input.rs b/crates/sargon-uniffi/src/security_center/support/input.rs new file mode 100644 index 000000000..414a78ef2 --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/support/input.rs @@ -0,0 +1,23 @@ +use crate::prelude::*; +use sargon::CheckSecurityProblemsInput as InternalCheckSecurityProblemsInput; +use sargon::IsCloudProfileSyncEnabled; + +#[derive(Clone, PartialEq, Eq, uniffi::Record, InternalConversion)] +pub struct CheckSecurityProblemsInput { + /// Whether the cloud profile sync is enabled. + pub is_cloud_profile_sync_enabled: bool, + + /// Addresses of entities that are unrecoverable. This is, the Factor Source used to create such entities + /// has not been backed up (e.g. seed phrase was not written down). + pub unrecoverable_entities: AddressesOfEntitiesInBadState, + + /// Addresses of entities that we don't have control over them. This is, the Factor Source used to create such entities + /// is missing (e.g. entity was imported but seed phrase never entered). + pub without_control_entities: AddressesOfEntitiesInBadState, + + /// Information about the latest backup made on the cloud. + pub last_cloud_backup: Option, + + /// Information about the latest backup made manually. + pub last_manual_backup: Option, +} diff --git a/crates/sargon-uniffi/src/security_center/support/mod.rs b/crates/sargon-uniffi/src/security_center/support/mod.rs new file mode 100644 index 000000000..d2fa050eb --- /dev/null +++ b/crates/sargon-uniffi/src/security_center/support/mod.rs @@ -0,0 +1,7 @@ +pub mod addresses_entities_bad_state; +mod backup_result; +mod input; + +pub use addresses_entities_bad_state::*; +pub use backup_result::*; +pub use input::*; diff --git a/crates/sargon-uniffi/src/system/sargon_os/mod.rs b/crates/sargon-uniffi/src/system/sargon_os/mod.rs index bda7c16a1..b19a85375 100644 --- a/crates/sargon-uniffi/src/system/sargon_os/mod.rs +++ b/crates/sargon-uniffi/src/system/sargon_os/mod.rs @@ -6,6 +6,7 @@ mod sargon_os_accounts; mod sargon_os_factors; mod sargon_os_gateway; mod sargon_os_profile; +mod sargon_os_security_center; mod sargon_os_security_structures; mod sargon_os_signing; mod sargon_os_sync_accounts; @@ -19,6 +20,7 @@ pub use sargon_os_accounts::*; pub use sargon_os_factors::*; pub use sargon_os_gateway::*; pub use sargon_os_profile::*; +pub use sargon_os_security_center::*; pub use sargon_os_security_structures::*; pub use sargon_os_signing::*; pub use sargon_os_sync_accounts::*; diff --git a/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_center.rs b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_center.rs new file mode 100644 index 000000000..2d24e7e43 --- /dev/null +++ b/crates/sargon-uniffi/src/system/sargon_os/sargon_os_security_center.rs @@ -0,0 +1,17 @@ +use crate::prelude::*; + +// ================== +// Check Security Problems +// ================== +#[uniffi::export] +impl SargonOS { + /// Returns all the `SecurityProblem`s that are present for the given input. + pub fn check_security_problems( + &self, + input: CheckSecurityProblemsInput, + ) -> Result> { + self.wrapped + .check_security_problems(input.into_internal()) + .into_result() + } +} diff --git a/crates/sargon/Cargo.toml b/crates/sargon/Cargo.toml index 7b01cb8ee..71742f201 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.74" +version = "1.1.75" edition = "2021" build = "build.rs" diff --git a/crates/sargon/src/core/types/bool_type.rs b/crates/sargon/src/core/types/bool_type.rs index d9feda30d..d23c848f5 100644 --- a/crates/sargon/src/core/types/bool_type.rs +++ b/crates/sargon/src/core/types/bool_type.rs @@ -38,6 +38,18 @@ macro_rules! decl_bool_type { &self.0 } } + + impl From for $name { + fn from(value: bool) -> Self { + $name(value) + } + } + + impl From<$name> for bool { + fn from(value: $name) -> bool { + value.0 + } + } }; } @@ -92,4 +104,13 @@ mod tests { example_false.0 = true; assert!(*example_false); } + + #[test] + fn from_into() { + let value = true; + assert!(ExampleTrue::from(value).0); + + let value = false; + assert!(!ExampleTrue::from(value).0); + } } diff --git a/crates/sargon/src/lib.rs b/crates/sargon/src/lib.rs index 0af2321b1..7f9b7feaf 100644 --- a/crates/sargon/src/lib.rs +++ b/crates/sargon/src/lib.rs @@ -17,6 +17,7 @@ mod home_cards; mod keys_collector; mod profile; mod radix_connect; +mod security_center; mod signing; mod system; mod types; @@ -32,6 +33,7 @@ pub mod prelude { pub use crate::keys_collector::*; pub use crate::profile::*; pub use crate::radix_connect::*; + pub use crate::security_center::*; pub use crate::signing::*; pub use crate::system::*; pub use crate::types::*; diff --git a/crates/sargon/src/security_center/client.rs b/crates/sargon/src/security_center/client.rs new file mode 100644 index 000000000..4653306ca --- /dev/null +++ b/crates/sargon/src/security_center/client.rs @@ -0,0 +1,343 @@ +use crate::prelude::*; + +pub struct SecurityCenterClient; + +impl SecurityCenterClient { + pub fn check_security_problems( + input: CheckSecurityProblemsInput, + ) -> Vec { + let mut problems = Vec::new(); + + let is_cloud_profile_sync_enabled = + *input.is_cloud_profile_sync_enabled; + + let has_problem_3 = || { + if input.unrecoverable_entities.is_empty() { + return None; + } + Some(input.unrecoverable_entities) + }; + + let has_problem_5 = || { + if !is_cloud_profile_sync_enabled { + return false; + } + let Some(cloud_backup) = input.last_cloud_backup else { + return true; + }; + *cloud_backup.is_failed + }; + + let has_problem_6 = || { + !is_cloud_profile_sync_enabled && input.last_manual_backup.is_none() + }; + + let has_problem_7 = || { + !is_cloud_profile_sync_enabled + && input + .last_manual_backup + .as_ref() + .map_or(false, |backup| !*backup.is_current) + }; + + let has_problem_9 = || { + if input.without_control_entities.is_empty() { + return None; + } + Some(input.without_control_entities) + }; + + if let Some(addresses) = has_problem_3() { + problems.push(SecurityProblem::Problem3 { addresses }); + } + + if has_problem_5() { + problems.push(SecurityProblem::Problem5); + } + + if has_problem_6() { + problems.push(SecurityProblem::Problem6); + } + + if has_problem_7() { + problems.push(SecurityProblem::Problem7); + } + + if let Some(addresses) = has_problem_9() { + problems.push(SecurityProblem::Problem9 { addresses }); + } + + problems + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityCenterClient; + + #[test] + fn problem_3() { + // Test without unrecoverable entities, we don't have Problem3 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled::sample(), + AddressesOfEntitiesInBadState::empty(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.iter().any(|problem| matches!( + problem, + SecurityProblem::Problem3 { .. } + ))); + + // Test with unrecoverable entities, we have Problem3 for the specified addresses + let addresses = AddressesOfEntitiesInBadState::sample(); + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled::sample(), + addresses.clone(), + AddressesOfEntitiesInBadState::empty(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem3 { addresses })); + } + + #[test] + fn problem_5() { + let failed_backup = BackupResult::new( + "f", + IsBackupResultCurrent(true), + IsBackupResultFailed(true), + ); + let success_backup = BackupResult::new( + "s", + IsBackupResultCurrent(true), + IsBackupResultFailed(false), + ); + + // Test with cloud profile sync disabled, we don't have Problem5 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + failed_backup.clone(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem5)); + + // Test with cloud profile sync enabled and success backup, we don't have Problem5 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + success_backup.clone(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem5)); + + // Test with cloud profile sync enabled and no backup, we have Problem5 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + None, + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem5)); + + // Test with cloud profile sync enabled and failed backup, we have Problem5 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + failed_backup.clone(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem5)); + } + + #[test] + fn problem_6() { + // Test with cloud profile sync enabled and last manual backup, we don't have Problem6 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem6)); + + // Test with cloud profile sync enabled and no last manual backup, we don't have Problem6 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + None, + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem6)); + + // Test with cloud profile sync disabled and with last manual backup, we don't have Problem6 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem6)); + + // Test with cloud profile sync disabled and no last manual backup, we have Problem6 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + None, + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem6)); + } + + #[test] + fn problem_7() { + let current_backup = BackupResult::new( + "c", + IsBackupResultCurrent(true), + IsBackupResultFailed(false), + ); + let outdated_backup = BackupResult::new( + "o", + IsBackupResultCurrent(false), + IsBackupResultFailed(false), + ); + + // Test with cloud profile sync enabled and no manual backup, we don't have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + None, + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem7)); + + // Test with cloud profile sync enabled and current manual backup, we don't have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + current_backup.clone(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem7)); + + // Test with cloud profile sync enabled and outdated manual backup, we don't have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(true), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + outdated_backup.clone(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem7)); + + // 99dasdasdasdasd + + // Test with cloud profile sync disabled and no manual backup, we don't have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + None, + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem7)); + + // Test with cloud profile sync disabled and current manual backup, we don't have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + current_backup.clone(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.contains(&SecurityProblem::Problem7)); + + // Test with cloud profile sync disabled and outdated manual backup, we have Problem7 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled(false), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::sample(), + BackupResult::sample(), + outdated_backup.clone(), + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem7)); + } + + #[test] + fn problem_9() { + // Test without unrecoverable entities, we don't have Problem9 + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled::sample(), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::empty(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(!result.iter().any(|problem| matches!( + problem, + SecurityProblem::Problem9 { .. } + ))); + + // Test with unrecoverable entities, we have Problem3 for the specified addresses + let addresses = AddressesOfEntitiesInBadState::sample(); + let input = CheckSecurityProblemsInput::new( + IsCloudProfileSyncEnabled::sample(), + addresses.clone(), + AddressesOfEntitiesInBadState::empty(), + BackupResult::sample(), + BackupResult::sample(), + ); + + let result = SUT::check_security_problems(input); + assert!(result.contains(&SecurityProblem::Problem3 { addresses })); + } +} diff --git a/crates/sargon/src/security_center/mod.rs b/crates/sargon/src/security_center/mod.rs new file mode 100644 index 000000000..a71e91342 --- /dev/null +++ b/crates/sargon/src/security_center/mod.rs @@ -0,0 +1,9 @@ +mod client; +mod security_problem; +mod security_problem_kind; +mod support; + +pub use client::*; +pub use security_problem::*; +pub use security_problem_kind::*; +pub use support::*; diff --git a/crates/sargon/src/security_center/security_problem.rs b/crates/sargon/src/security_center/security_problem.rs new file mode 100644 index 000000000..02ecf3009 --- /dev/null +++ b/crates/sargon/src/security_center/security_problem.rs @@ -0,0 +1,148 @@ +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq, derive_more::Display)] + +/// An enum describing each potential Security Problem the Wallet can encounter. +/// +/// See [the Confluence doc for details][doc]. +/// +/// [doc]: https://radixdlt.atlassian.net/wiki/spaces/AT/pages/3392569357/Security-related+Problem+States+in+the+Wallet +pub enum SecurityProblem { + /// The given addresses of `accounts` and `personas` are unrecoverable if the user loses their phone, since their corresponding seed phrase has not been written down. + /// NOTE: This definition differs from the one at Confluence since we don't have shields implemented yet. + #[display("Problem3")] + Problem3 { + addresses: AddressesOfEntitiesInBadState, + }, + + /// Wallet backups to the cloud aren’t working (wallet tried to do a backup, and it didn’t work within, say, 5 minutes.) + /// This means that currently all accounts and personas are at risk of being practically unrecoverable if the user loses their phone. + /// Also they would lose all of their other non-security wallet settings and data. + Problem5, + + /// Cloud backups are turned off and user has never done a manual file export. This means that currently all accounts and personas are at risk of + /// being practically unrecoverable if the user loses their phone. Also, they would lose all of their other non-security wallet settings and data. + Problem6, + + /// Cloud backups are turned off and user previously did a manual file export, but has made a change and haven’t yet re-exported a file backup that + /// includes that change. This means that any changes made will be lost if the user loses their phone - including control of new accounts/personas they’ve + /// created, as well as changed settings or changed/added data. + Problem7, + + /// User has gotten a new phone (and restored their wallet from backup) and the wallet sees that there are accounts without shields using a phone key, + /// meaning they can only be recovered with the seed phrase. (See problem 2) This would also be the state if a user disabled their PIN (and reenabled it), clearing phone keys. + #[display("Problem9")] + Problem9 { + addresses: AddressesOfEntitiesInBadState, + }, +} + +impl SecurityProblem { + pub fn id(&self) -> u64 { + match self { + SecurityProblem::Problem3 { .. } => 3, + SecurityProblem::Problem5 => 5, + SecurityProblem::Problem6 => 6, + SecurityProblem::Problem7 => 7, + SecurityProblem::Problem9 { .. } => 9, + } + } +} + +impl SecurityProblem { + pub fn kind(&self) -> SecurityProblemKind { + match self { + SecurityProblem::Problem3 { .. } + | SecurityProblem::Problem9 { .. } => { + SecurityProblemKind::SecurityFactors + } + SecurityProblem::Problem5 + | SecurityProblem::Problem6 + | SecurityProblem::Problem7 => { + SecurityProblemKind::ConfigurationBackup + } + } + } +} + +impl HasSampleValues for SecurityProblem { + fn sample() -> Self { + Self::Problem3 { + addresses: AddressesOfEntitiesInBadState::sample(), + } + } + + fn sample_other() -> Self { + Self::Problem5 + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityProblem; + + #[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 id() { + assert_eq!( + SUT::Problem3 { + addresses: AddressesOfEntitiesInBadState::sample() + } + .id(), + 3 + ); + assert_eq!(SUT::Problem5.id(), 5); + assert_eq!(SUT::Problem6.id(), 6); + assert_eq!(SUT::Problem7.id(), 7); + assert_eq!( + SUT::Problem9 { + addresses: AddressesOfEntitiesInBadState::sample() + } + .id(), + 9 + ); + } + + #[test] + fn kind() { + assert_eq!( + SUT::Problem3 { + addresses: AddressesOfEntitiesInBadState::sample() + } + .kind(), + SecurityProblemKind::SecurityFactors + ); + assert_eq!( + SUT::Problem5.kind(), + SecurityProblemKind::ConfigurationBackup + ); + assert_eq!( + SUT::Problem6.kind(), + SecurityProblemKind::ConfigurationBackup + ); + assert_eq!( + SUT::Problem7.kind(), + SecurityProblemKind::ConfigurationBackup + ); + assert_eq!( + SUT::Problem9 { + addresses: AddressesOfEntitiesInBadState::sample() + } + .kind(), + SecurityProblemKind::SecurityFactors + ); + } +} diff --git a/crates/sargon/src/security_center/security_problem_kind.rs b/crates/sargon/src/security_center/security_problem_kind.rs new file mode 100644 index 000000000..fd8f1c089 --- /dev/null +++ b/crates/sargon/src/security_center/security_problem_kind.rs @@ -0,0 +1,38 @@ +use crate::prelude::*; + +#[derive(Clone, Debug, PartialEq)] +/// An enum describing the different types of Security Problems the Wallet can encounter. +pub enum SecurityProblemKind { + SecurityFactors, + + ConfigurationBackup, +} + +impl HasSampleValues for SecurityProblemKind { + fn sample() -> Self { + Self::SecurityFactors + } + + fn sample_other() -> Self { + Self::ConfigurationBackup + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SecurityProblemKind; + + #[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/security_center/support/addresses_entities_bad_state.rs b/crates/sargon/src/security_center/support/addresses_entities_bad_state.rs new file mode 100644 index 000000000..86e01b01c --- /dev/null +++ b/crates/sargon/src/security_center/support/addresses_entities_bad_state.rs @@ -0,0 +1,87 @@ +use crate::prelude::*; + +/// A struct that represents the addresses of entities in a bad state. +#[derive(Debug, Clone, PartialEq)] +pub struct AddressesOfEntitiesInBadState { + pub accounts: Vec, + pub hidden_accounts: Vec, + pub personas: Vec, + pub hidden_personas: Vec, +} + +impl AddressesOfEntitiesInBadState { + pub fn new( + accounts: impl IntoIterator, + hidden_accounts: impl IntoIterator, + personas: impl IntoIterator, + hidden_personas: impl IntoIterator, + ) -> Self { + Self { + accounts: Vec::from_iter(accounts), + hidden_accounts: Vec::from_iter(hidden_accounts), + personas: Vec::from_iter(personas), + hidden_personas: Vec::from_iter(hidden_personas), + } + } + + pub fn empty() -> Self { + Self::new([], [], [], []) + } + + pub fn is_empty(&self) -> bool { + self.accounts.is_empty() + && self.hidden_accounts.is_empty() + && self.personas.is_empty() // if it only contains hidden_personas, we don't consider it empty + } +} + +impl HasSampleValues for AddressesOfEntitiesInBadState { + fn sample() -> Self { + Self { + accounts: Vec::<_>::sample(), + hidden_accounts: Vec::new(), + personas: Vec::<_>::sample(), + hidden_personas: Vec::new(), + } + } + + fn sample_other() -> Self { + Self { + accounts: Vec::new(), + hidden_accounts: Vec::sample_other(), + personas: Vec::new(), + hidden_personas: Vec::sample_other(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = AddressesOfEntitiesInBadState; + + #[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 is_empty() { + let sut = SUT::sample(); + assert!(!sut.is_empty()); + + let sut = SUT::empty(); + assert!(sut.is_empty()); + + let sut = SUT::new([], [], [], Vec::sample()); + assert!(sut.is_empty()); + } +} diff --git a/crates/sargon/src/security_center/support/backup_result.rs b/crates/sargon/src/security_center/support/backup_result.rs new file mode 100644 index 000000000..607eec8d8 --- /dev/null +++ b/crates/sargon/src/security_center/support/backup_result.rs @@ -0,0 +1,70 @@ +use crate::prelude::*; + +/// A struct that represents the result of a given backup. +/// +/// Reference for iOS: it is a combination of `BackupStatus` and `BackupResult` (all in one). +#[derive(Debug, Clone, PartialEq)] +pub struct BackupResult { + /// The identifier of the backup. + pub save_identifier: String, + + /// Whether this backup matches the one on Profile. + pub is_current: IsBackupResultCurrent, + + /// Whether this backup has failed. + pub is_failed: IsBackupResultFailed, +} + +decl_bool_type!(IsBackupResultCurrent, false); +decl_bool_type!(IsBackupResultFailed, false); + +impl BackupResult { + pub fn new( + save_identifier: impl AsRef, + is_current: IsBackupResultCurrent, + is_failed: IsBackupResultFailed, + ) -> Self { + Self { + save_identifier: save_identifier.as_ref().to_owned(), + is_current, + is_failed, + } + } +} + +impl HasSampleValues for BackupResult { + fn sample() -> Self { + Self::new( + String::sample(), + IsBackupResultCurrent(true), + IsBackupResultFailed(false), + ) + } + + fn sample_other() -> Self { + Self::new( + String::sample_other(), + IsBackupResultCurrent(false), + IsBackupResultFailed(true), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = BackupResult; + + #[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/security_center/support/input.rs b/crates/sargon/src/security_center/support/input.rs new file mode 100644 index 000000000..9a0f1e6a4 --- /dev/null +++ b/crates/sargon/src/security_center/support/input.rs @@ -0,0 +1,80 @@ +use crate::prelude::*; + +#[derive(Debug, Clone, PartialEq)] +pub struct CheckSecurityProblemsInput { + /// Whether the cloud profile sync is enabled. + pub is_cloud_profile_sync_enabled: IsCloudProfileSyncEnabled, + + /// Addresses of entities that are unrecoverable. This is, the Factor Source used to create such entities + /// has not been backed up (e.g. seed phrase was not written down). + pub unrecoverable_entities: AddressesOfEntitiesInBadState, + + /// Addresses of entities that we don't have control over them. This is, the Factor Source used to create such entities + /// is missing (e.g. entity was imported but seed phrase never entered). + pub without_control_entities: AddressesOfEntitiesInBadState, + + /// Information about the latest backup made on the cloud. + pub last_cloud_backup: Option, + + /// Information about the latest backup made manually. + pub last_manual_backup: Option, +} + +impl CheckSecurityProblemsInput { + pub fn new( + is_cloud_profile_sync_enabled: IsCloudProfileSyncEnabled, + unrecoverable_entities: AddressesOfEntitiesInBadState, + without_control_entities: AddressesOfEntitiesInBadState, + last_cloud_backup: impl Into>, + last_manual_backup: impl Into>, + ) -> Self { + Self { + is_cloud_profile_sync_enabled, + unrecoverable_entities, + without_control_entities, + last_cloud_backup: last_cloud_backup.into(), + last_manual_backup: last_manual_backup.into(), + } + } +} + +impl HasSampleValues for CheckSecurityProblemsInput { + fn sample() -> Self { + Self::new( + IsCloudProfileSyncEnabled::sample(), + AddressesOfEntitiesInBadState::sample(), + AddressesOfEntitiesInBadState::empty(), + BackupResult::sample(), + None, + ) + } + + fn sample_other() -> Self { + Self::new( + IsCloudProfileSyncEnabled::sample_other(), + AddressesOfEntitiesInBadState::empty(), + AddressesOfEntitiesInBadState::sample_other(), + None, + BackupResult::sample_other(), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = CheckSecurityProblemsInput; + + #[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/security_center/support/mod.rs b/crates/sargon/src/security_center/support/mod.rs new file mode 100644 index 000000000..d2fa050eb --- /dev/null +++ b/crates/sargon/src/security_center/support/mod.rs @@ -0,0 +1,7 @@ +pub mod addresses_entities_bad_state; +mod backup_result; +mod input; + +pub use addresses_entities_bad_state::*; +pub use backup_result::*; +pub use input::*; diff --git a/crates/sargon/src/system/sargon_os/mod.rs b/crates/sargon/src/system/sargon_os/mod.rs index 51f706821..6fd70fdec 100644 --- a/crates/sargon/src/system/sargon_os/mod.rs +++ b/crates/sargon/src/system/sargon_os/mod.rs @@ -7,6 +7,7 @@ mod sargon_os_factors; mod sargon_os_gateway; mod sargon_os_personas; mod sargon_os_profile; +mod sargon_os_security_center; mod sargon_os_security_structures; mod sargon_os_signing; mod sargon_os_sync_accounts; @@ -21,6 +22,7 @@ pub use sargon_os_factors::*; pub use sargon_os_gateway::*; pub use sargon_os_personas::*; pub use sargon_os_profile::*; +pub use sargon_os_security_center::*; pub use sargon_os_security_structures::*; pub use sargon_os_signing::*; pub use sargon_os_sync_accounts::*; diff --git a/crates/sargon/src/system/sargon_os/sargon_os_security_center.rs b/crates/sargon/src/system/sargon_os/sargon_os_security_center.rs new file mode 100644 index 000000000..f7aaa510c --- /dev/null +++ b/crates/sargon/src/system/sargon_os/sargon_os_security_center.rs @@ -0,0 +1,45 @@ +use crate::prelude::*; + +// ================== +// Check Security Problems +// ================== +impl SargonOS { + /// Returns all the `SecurityProblem`s that are present for the given input. + pub fn check_security_problems( + &self, + input: CheckSecurityProblemsInput, + ) -> Result> { + Ok(SecurityCenterClient::check_security_problems(input)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[allow(clippy::upper_case_acronyms)] + type SUT = SargonOS; + + #[actix_rt::test] + async fn check_problems() { + let os = boot().await; + + let input = CheckSecurityProblemsInput::sample(); + let result = os.check_security_problems(input.clone()).unwrap(); + assert_eq!( + result, + SecurityCenterClient::check_security_problems(input) + ); + } + + async fn boot() -> Arc { + let req = SUT::boot_test_with_networking_driver(Arc::new( + MockNetworkingDriver::new_always_failing(), + )); + + actix_rt::time::timeout(SARGON_OS_TEST_MAX_ASYNC_DURATION, req) + .await + .unwrap() + .unwrap() + } +}