Skip to content

Commit

Permalink
[ABW-3396] Claim Wallet (#1161)
Browse files Browse the repository at this point in the history
Co-authored-by: matiasbzurovski <164921079+matiasbzurovski@users.noreply.github.com>
Co-authored-by: Matias Bzurovski <matias.bzurovski@rdx.works>
  • Loading branch information
3 people committed Jun 14, 2024
1 parent c06e24e commit fb1e107
Show file tree
Hide file tree
Showing 8 changed files with 45 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ public struct DeviceFactorSourceClient: Sendable {
/// Fetched accounts and personas on current network that are controlled by a device factor source, for every factor source in current profile
public var controlledEntities: GetControlledEntities

/// The entities (`Accounts` & `Personas`) that are problematic. This is, that either:
/// The entities (`Accounts` & `Personas`) that are in bad state. This is, that either:
/// - their mnmemonic is missing (entity was imported but seed phrase never entered), or
/// - their mnmemonic is not backed up (entity was created but seed phrase never written down).
public var problematicEntities: ProblematicEntities
public var entitiesInBadState: EntitiesInBadState

public init(
publicKeysFromOnDeviceHD: @escaping PublicKeysFromOnDeviceHD,
signatureFromOnDeviceHD: @escaping SignatureFromOnDeviceHD,
isAccountRecoveryNeeded: @escaping IsAccountRecoveryNeeded,
entitiesControlledByFactorSource: @escaping GetEntitiesControlledByFactorSource,
controlledEntities: @escaping GetControlledEntities,
problematicEntities: @escaping ProblematicEntities
entitiesInBadState: @escaping EntitiesInBadState
) {
self.publicKeysFromOnDeviceHD = publicKeysFromOnDeviceHD
self.signatureFromOnDeviceHD = signatureFromOnDeviceHD
self.isAccountRecoveryNeeded = isAccountRecoveryNeeded
self.entitiesControlledByFactorSource = entitiesControlledByFactorSource
self.controlledEntities = controlledEntities
self.problematicEntities = problematicEntities
self.entitiesInBadState = entitiesInBadState
}
}

Expand All @@ -42,7 +42,7 @@ extension DeviceFactorSourceClient {
public typealias PublicKeysFromOnDeviceHD = @Sendable (PublicKeysFromOnDeviceHDRequest) async throws -> [HierarchicalDeterministicPublicKey]
public typealias SignatureFromOnDeviceHD = @Sendable (SignatureFromOnDeviceHDRequest) async throws -> SignatureWithPublicKey
public typealias IsAccountRecoveryNeeded = @Sendable () async throws -> Bool
public typealias ProblematicEntities = @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)>
public typealias EntitiesInBadState = @Sendable () async throws -> AnyAsyncSequence<(withoutControl: AddressesOfEntitiesInBadState, unrecoverable: AddressesOfEntitiesInBadState)>
}

// MARK: - DiscrepancyUnsupportedCurve
Expand Down Expand Up @@ -241,8 +241,8 @@ extension SigningPurpose {
// MARK: - FactorInstanceDoesNotHaveADerivationPathUnableToSign
struct FactorInstanceDoesNotHaveADerivationPathUnableToSign: Swift.Error {}

// MARK: - ProblematicAddresses
public struct ProblematicAddresses: Sendable, Hashable {
// MARK: - AddressesOfEntitiesInBadState
public struct AddressesOfEntitiesInBadState: Sendable, Hashable {
let accounts: [AccountAddress]
let hiddenAccounts: [AccountAddress]
let personas: [IdentityAddress]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,33 @@ extension DeviceFactorSourceClient: DependencyKey {
)
}

struct FactorSourceHasMnemonic: Sendable, Equatable {
struct KeychainPresenceOfMnemonic: Sendable, Equatable {
let id: FactorSourceIDFromHash
let present: Bool
}

@Sendable
func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[FactorSourceHasMnemonic]> {
func factorSourcesMnemonicPresence() async -> AnyAsyncSequence<[KeychainPresenceOfMnemonic]> {
await combineLatest(profileStore.factorSourcesValues(), secureStorageClient.keychainChanged().prepend(()))
.map { factorSources, _ in
factorSources
.compactMap { $0.extract(DeviceFactorSource.self)?.id }
.map { id in
FactorSourceHasMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id))
KeychainPresenceOfMnemonic(id: id, present: secureStorageClient.containsMnemonicIdentifiedByFactorSourceID(id))
}
}
.removeDuplicates()
.eraseToAnyAsyncSequence()
}

let problematicEntities: @Sendable () async throws -> AnyAsyncSequence<(mnemonicMissing: ProblematicAddresses, unrecoverable: ProblematicAddresses)> = {
await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { factorSources, backedUpFactorSources, profile in
let entitiesInBadState: @Sendable () async throws -> AnyAsyncSequence<(withoutControl: AddressesOfEntitiesInBadState, unrecoverable: AddressesOfEntitiesInBadState)> = {
await combineLatest(factorSourcesMnemonicPresence(), userDefaults.factorSourceIDOfBackedUpMnemonics(), profileStore.values()).map { presencesOfMnemonics, backedUpFactorSources, profile in

let mnemonicMissingFactorSources = factorSources
let mnemonicMissingFactorSources = presencesOfMnemonics
.filter(not(\.present))
.map(\.id)

let mnemomincPresentFactorSources = factorSources
let mnemomincPresentFactorSources = presencesOfMnemonics
.filter(\.present)
.map(\.id)

Expand All @@ -112,49 +112,35 @@ extension DeviceFactorSourceClient: DependencyKey {
let personas = network.getPersonas()
let hiddenPersonas = network.getHiddenPersonas()

func mnemonicMissing(_ account: Account) -> Bool {
switch account.securityState {
func withoutControl(_ entity: some EntityProtocol) -> Bool {
switch entity.securityState {
case let .unsecured(value):
mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId)
}
}

func mnemonicMissing(_ persona: Persona) -> Bool {
switch persona.securityState {
case let .unsecured(value):
mnemonicMissingFactorSources.contains(value.transactionSigning.factorSourceId)
}
}

func unrecoverable(_ account: Account) -> Bool {
switch account.securityState {
case let .unsecured(value):
unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId)
}
}

func unrecoverable(_ persona: Persona) -> Bool {
switch persona.securityState {
func unrecoverable(_ entity: some EntityProtocol) -> Bool {
switch entity.securityState {
case let .unsecured(value):
unrecoverableFactorSources.contains(value.transactionSigning.factorSourceId)
}
}

let mnemonicMissing = ProblematicAddresses(
accounts: accounts.filter(mnemonicMissing(_:)).map(\.address),
hiddenAccounts: hiddenAccounts.filter(mnemonicMissing(_:)).map(\.address),
personas: personas.filter(mnemonicMissing(_:)).map(\.address),
hiddenPersonas: hiddenPersonas.filter(mnemonicMissing(_:)).map(\.address)
let withoutControl = AddressesOfEntitiesInBadState(
accounts: accounts.filter(withoutControl(_:)).map(\.address),
hiddenAccounts: hiddenAccounts.filter(withoutControl(_:)).map(\.address),
personas: personas.filter(withoutControl(_:)).map(\.address),
hiddenPersonas: hiddenPersonas.filter(withoutControl(_:)).map(\.address)
)

let unrecoverable = ProblematicAddresses(
let unrecoverable = AddressesOfEntitiesInBadState(
accounts: accounts.filter(unrecoverable(_:)).map(\.address),
hiddenAccounts: hiddenAccounts.filter(unrecoverable(_:)).map(\.address),
personas: personas.filter(unrecoverable(_:)).map(\.address),
hiddenPersonas: hiddenPersonas.filter(unrecoverable(_:)).map(\.address)
)

return (mnemonicMissing: mnemonicMissing, unrecoverable: unrecoverable)
return (withoutControl: withoutControl, unrecoverable: unrecoverable)
}
.eraseToAnyAsyncSequence()
}
Expand Down Expand Up @@ -219,7 +205,7 @@ extension DeviceFactorSourceClient: DependencyKey {
try await entitiesControlledByFactorSource($0, maybeOverridingSnapshot)
})
},
problematicEntities: problematicEntities
entitiesInBadState: entitiesInBadState
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension DeviceFactorSourceClient: TestDependencyKey {
isAccountRecoveryNeeded: { false },
entitiesControlledByFactorSource: { _, _ in throw NoopError() },
controlledEntities: { _ in [] },
problematicEntities: { throw NoopError() }
entitiesInBadState: { throw NoopError() }
)

public static let testValue = Self(
Expand All @@ -25,11 +25,11 @@ extension DeviceFactorSourceClient: TestDependencyKey {
isAccountRecoveryNeeded: unimplemented("\(Self.self).isAccountRecoveryNeeded"),
entitiesControlledByFactorSource: unimplemented("\(Self.self).entitiesControlledByFactorSource"),
controlledEntities: unimplemented("\(Self.self).controlledEntities"),
problematicEntities: unimplemented("\(Self.self).problematicEntities")
entitiesInBadState: unimplemented("\(Self.self).entitiesInBadState")
)
}

private extension ProblematicAddresses {
private extension AddressesOfEntitiesInBadState {
static var empty: Self {
.init(accounts: [], hiddenAccounts: [], personas: [], hiddenPersonas: [])
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public struct KeychainClient: Sendable {
public var _getAllKeysMatchingAttributes: GetAllKeysMatchingAttributes
public var _keychainChanged: KeychainChanged

/// This a _best effort_ publisher that will emit a change every time the Keychain is changed due to actions inside the Wallet app.
/// However, we cannot detect external changes (e.g. Keychain getting wiped when passcode is deleted).
public var _keychainChanged: KeychainChanged

public init(
getServiceAndAccessGroup: @escaping GetServiceAndAccessGroup,
containsDataForKey: @escaping ContainsDataForKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ extension SecurityCenterClient {
public enum SecurityProblem: Hashable, Sendable, Identifiable {
/// 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.
case problem3(addresses: ProblematicAddresses)
case 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.
Expand All @@ -38,7 +38,7 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable {
case 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.
case problem9(addresses: ProblematicAddresses)
case problem9(addresses: AddressesOfEntitiesInBadState)

public var id: Int { number }

Expand Down Expand Up @@ -76,7 +76,7 @@ public enum SecurityProblem: Hashable, Sendable, Identifiable {
}
}

private func problem3(addresses: ProblematicAddresses) -> String {
private func problem3(addresses: AddressesOfEntitiesInBadState) -> String {
typealias Common = L10n.SecurityProblems.Common
typealias Problem = L10n.SecurityProblems.No3
let hasHidden = addresses.hiddenAccounts.count + addresses.hiddenPersonas.count > 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ extension SecurityCenterClient {
@Sendable
func startMonitoring() async throws {
let profileValues = await profileStore.values()
let problematicValues = try await deviceFactorSourceClient.problematicEntities()
let entitiesInBadState = try await deviceFactorSourceClient.entitiesInBadState()
let backupValues = await combineLatest(cloudBackups(), manualBackups()).map { (cloud: $0, manual: $1) }

for try await (profile, problematic, backups) in combineLatest(profileValues, problematicValues, backupValues) {
for try await (profile, entitiesInBadState, backups) in combineLatest(profileValues, entitiesInBadState, backupValues) {
let isCloudProfileSyncEnabled = profile.appPreferences.security.isCloudProfileSyncEnabled

func hasProblem3() async -> ProblematicAddresses? {
problematic.unrecoverable.isEmpty ? nil : problematic.unrecoverable
func hasProblem3() async -> AddressesOfEntitiesInBadState? {
entitiesInBadState.unrecoverable.isEmpty ? nil : entitiesInBadState.unrecoverable
}

func hasProblem5() -> Bool {
Expand All @@ -81,8 +81,8 @@ extension SecurityCenterClient {
!isCloudProfileSyncEnabled && backups.manual?.isCurrent == false
}

func hasProblem9() async -> ProblematicAddresses? {
problematic.mnemonicMissing.isEmpty ? nil : problematic.mnemonicMissing
func hasProblem9() async -> AddressesOfEntitiesInBadState? {
entitiesInBadState.withoutControl.isEmpty ? nil : entitiesInBadState.withoutControl
}

var result: [SecurityProblem] = []
Expand Down Expand Up @@ -117,7 +117,7 @@ extension SecurityCenterClient {
}
}

private extension ProblematicAddresses {
private extension AddressesOfEntitiesInBadState {
var isEmpty: Bool {
accounts.count + hiddenAccounts.count + personas.count == 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct TransportProfileClient: Sendable {
}

extension TransportProfileClient {
public typealias ImportProfile = @Sendable (Profile, Set<FactorSourceIDFromHash>, Bool) async throws -> Void
public typealias ImportProfile = @Sendable (Profile, Set<FactorSourceIDFromHash>, _ containsP2PLinks: Bool) async throws -> Void
public typealias ProfileForExport = @Sendable () async throws -> Profile
public typealias DidExportProfile = @Sendable (Profile) throws -> Void
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ private extension [SecurityProblem] {
}
}

private extension ProblematicAddresses {
private extension AddressesOfEntitiesInBadState {
var problematicAccounts: Set<AccountAddress> {
Set(accounts + hiddenAccounts)
}
Expand Down

0 comments on commit fb1e107

Please sign in to comment.