Skip to content

Commit

Permalink
Merged main
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasbzurovski committed Jun 14, 2024
2 parents c06e24e + 8cd380f commit d2fd058
Show file tree
Hide file tree
Showing 37 changed files with 83 additions and 82 deletions.
15 changes: 11 additions & 4 deletions RadixWallet/Clients/CloudBackupClient/CloudBackupClient+Live.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ extension CloudBackupClient {

@Sendable
func backupProfileAndSaveResult(_ profile: Profile, existingRecord: CKRecord?) async throws {
try? userDefaults.setLastCloudBackup(.started(.now), of: profile)
try? userDefaults.setLastCloudBackup(.started(.now), of: profile.header)

do {
let json = profile.toJSONString()
Expand All @@ -138,18 +138,25 @@ extension CloudBackupClient {
failure = .other
}

try? userDefaults.setLastCloudBackup(.failure(failure), of: profile)
try? userDefaults.setLastCloudBackup(.failure(failure), of: profile.header)
throw error
}

try? userDefaults.setLastCloudBackup(.success, of: profile)
try? userDefaults.setLastCloudBackup(.success, of: profile.header)
}

@Sendable
func performAutomaticBackup(_ profile: Profile, timeToCheckIfClaimed: Bool) async {
let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty
let existingRecord = try? await fetchProfileRecord(profile.id)
let backedUpHeader = try? existingRecord.map(getProfileHeader)

if let backedUpHeader, let backupDate = existingRecord?.modificationDate {
try? userDefaults.setLastCloudBackup(.success, of: backedUpHeader, at: backupDate)
} else {
try? userDefaults.removeLastCloudBackup(for: profile.id)
}

let needsBackUp = profile.appPreferences.security.isCloudProfileSyncEnabled && profile.header.isNonEmpty
let isBackedUp = backedUpHeader?.saveIdentifier == profile.header.saveIdentifier
let shouldBackUp = needsBackUp && !isBackedUp

Expand Down
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 @@ -421,7 +421,6 @@ extension FactorSourceKind: Comparable {
case .offDeviceMnemonic: 1
case .securityQuestions: 2
case .trustedContact: 3

// we want to sign with device last, since it would allow for us to stop using
// ephemeral notary and allow us to implement a AutoPurgingMnemonicCache which
// deletes items after 1 sec, thus `device` must come last.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ public struct KeychainClient: Sendable {
public var _removeDataForKey: RemoveDataForKey
public var _removeAllItems: RemoveAllItems
public var _getAllKeysMatchingAttributes: GetAllKeysMatchingAttributes

/// 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(
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 @@ -47,6 +47,7 @@ extension TransactionFailure {
case .failedToSignIntentWithAccountSigners, .failedToSignSignedCompiledIntentWithNotarySigner, .failedToConvertNotarySignature, .failedToConvertAccountSignatures:
(errorKind: .failedToSignTransaction, message: nil)
}

case .failedToSubmit:
(errorKind: .failedToSubmitTransaction, message: nil)

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 @@ -165,13 +165,12 @@ extension UserDefaults.Dependency {
try save(codable: backups, forKey: .lastCloudBackups)
}

public func setLastCloudBackup(_ result: BackupResult.Result, of profile: Profile) throws {
public func setLastCloudBackup(_ result: BackupResult.Result, of header: Profile.Header, at date: Date = .now) throws {
var backups: [UUID: BackupResult] = getLastCloudBackups
let now = Date.now
let lastSuccess = result == .success ? now : backups[profile.id]?.lastSuccess
backups[profile.id] = .init(
date: now,
saveIdentifier: profile.header.saveIdentifier,
let lastSuccess = result == .success ? date : backups[header.id]?.lastSuccess
backups[header.id] = .init(
date: date,
saveIdentifier: header.saveIdentifier,
result: result,
lastSuccess: lastSuccess
)
Expand Down
1 change: 0 additions & 1 deletion RadixWallet/Core/DesignSystem/Components/Thumbnails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ public struct LoadableImage<Placeholder: View>: View {
case .shimmer:
Color.app.gray4
.shimmer(active: true, config: .accountResourcesLoading)

case let .color(color):
color
case let .asset(imageAsset):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ extension P2P.RTCIncomingMessageContainer {
case let (.failure(lhsFailure), .failure(rhsFailure)):
// FIXME: strongly type messages? to an Error type which is Hashable?
return String(describing: lhsFailure) == String(describing: rhsFailure)

case let (.success(lhsSuccess), .success(rhsSuccess)):
return lhsSuccess == rhsSuccess

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ extension P2P.ConnectorExtension.Response.LedgerHardwareWallet {
self.response = try decodeResponse {
Success.signChallenge($0)
}

case .deriveAndDisplayAddress:
self.response = try decodeResponse {
Success.deriveAndDisplayAddress($0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ public struct DevAccountPreferences: Sendable, FeatureReducer {
case let .canCreateAuthSigningKey(canCreateAuthSigningKey):
state.canCreateAuthSigningKey = canCreateAuthSigningKey
return .none

case let .canTurnIntoDappDefAccountType(canTurnIntoDappDefAccountType):
state.canTurnIntoDappDefinitionAccountType = canTurnIntoDappDefAccountType
return .none
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ public struct AccountRecoveryScanCoordinator: Sendable, FeatureReducer {
let childState = state.backTo ?? AccountRecoveryScanCoordinator.State.accountRecoveryScanInProgressState(purpose: state.purpose)
state.root = .accountRecoveryScanInProgress(childState)
return .none

case let .selectInactiveAccountsToAdd(.delegate(.finished(selectedInactive, active))):
return completed(purpose: state.purpose, active: active, inactive: selectedInactive)

Expand Down
1 change: 1 addition & 0 deletions RadixWallet/Features/AppFeature/App+Reducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public struct App: Sendable, FeatureReducer {
} else {
goToMain(state: &state)
}

default:
.none
}
Expand Down
Loading

0 comments on commit d2fd058

Please sign in to comment.