diff --git a/TangemSdk/TangemSdk/Operations/Backup/BackupService.swift b/TangemSdk/TangemSdk/Operations/Backup/BackupService.swift index fad4885b..6255fea7 100644 --- a/TangemSdk/TangemSdk/Operations/Backup/BackupService.swift +++ b/TangemSdk/TangemSdk/Operations/Backup/BackupService.swift @@ -19,7 +19,7 @@ public class BackupService { addedBackupCardsCount < BackupService.maxBackupCardsCount && repo.data.primaryCard?.linkingKey != nil } - + public var hasIncompletedBackup: Bool { switch currentState { case .finalizingPrimaryCard, .finalizingBackupCard: @@ -28,12 +28,12 @@ public class BackupService { return false } } - + public var config: Config { get { sdk.config } set { sdk.config = newValue } } - + public var addedBackupCardsCount: Int { repo.data.backupCards.count } public var canProceed: Bool { currentState != .preparing && currentState != .finished } public var accessCodeIsSet: Bool { repo.data.accessCode != nil } @@ -41,53 +41,53 @@ public class BackupService { public var primaryCardIsSet: Bool { repo.data.primaryCard != nil } public var primaryCard: PrimaryCard? { repo.data.primaryCard } public var backupCardIds: [String] { repo.data.backupCards.map {$0.cardId} } - + /// Perform additional compatibility checks while adding backup cards. Change this setting only if you understand what you do. public var skipCompatibilityChecks: Bool = false - + private let sdk: TangemSdk private var repo: BackupRepo = .init() private var currentCommand: AnyObject? = nil private var handleErrors: Bool { sdk.config.handleErrors } - + public init(sdk: TangemSdk) { self.sdk = sdk self.updateState() } - + deinit { Log.debug("BackupService deinit") } - + public func discardIncompletedBackup() { repo.reset() updateState() } - + public func addBackupCard(completion: @escaping CompletionResult) { guard let primaryCard = repo.data.primaryCard else { completion(.failure(.missingPrimaryCard)) return } - + if handleErrors { guard addedBackupCardsCount < BackupService.maxBackupCardsCount else { completion(.failure(.tooMuchBackupCards)) return } } - + if primaryCard.certificate != nil { readBackupCard(primaryCard, completion: completion) return } - + fetchCertificate( for: primaryCard.cardId, cardPublicKey: primaryCard.cardPublicKey, firmwareVersion: primaryCard.firmwareVersion) { [weak self] result in guard let self else { return } - + switch result { case .success(let certificate): var primaryCard = primaryCard @@ -99,59 +99,59 @@ public class BackupService { } } } - + public func setAccessCode(_ code: String) throws { repo.data.accessCode = nil let code = code.trim() - + if handleErrors { guard !code.isEmpty else { throw TangemSdkError.accessCodeRequired } - + if code == UserCodeType.accessCode.defaultValue { throw TangemSdkError.accessCodeCannotBeDefault } - + if code.count < UserCodeType.minLength { throw TangemSdkError.accessCodeTooShort } } - + guard currentState == .preparing || currentState == .finalizingPrimaryCard else { throw TangemSdkError.accessCodeCannotBeChanged } - + repo.data.accessCode = code.sha256() updateState() } - + public func setPasscode(_ code: String) throws { repo.data.passcode = nil let code = code.trim() - + if handleErrors { guard !code.isEmpty else { throw TangemSdkError.passcodeRequired } - + if code == UserCodeType.passcode.defaultValue { throw TangemSdkError.passcodeCannotBeDefault } - + if code.count < UserCodeType.minLength { throw TangemSdkError.passcodeTooShort } } - + guard currentState == .preparing || currentState == .finalizingPrimaryCard else { throw TangemSdkError.passcodeCannotBeChanged } - + repo.data.passcode = code.sha256() updateState() } - + public func proceedBackup(completion: @escaping CompletionResult) { switch currentState { case .finalizingPrimaryCard: @@ -166,20 +166,20 @@ public class BackupService { completion(.failure(TangemSdkError.backupServiceInvalidState)) } } - + public func setPrimaryCard(_ primaryCard: PrimaryCard) { repo.data.primaryCard = primaryCard updateState() } - + public func readPrimaryCard(cardId: String? = nil, completion: @escaping CompletionResult) { let formattedCardId = cardId.flatMap { CardIdFormatter(style: sdk.config.cardIdDisplayFormat).string(from: $0) } - + let initialMessage = formattedCardId.map { Message(header: nil, body: "backup_prepare_primary_card_message_format".localized($0)) } ?? Message(header: "backup_prepare_primary_card_message".localized) - + let command = StartPrimaryCardLinkingCommand() currentCommand = command sdk.startSession(with: command, @@ -197,7 +197,7 @@ public class BackupService { self.currentCommand = nil } } - + private func handleCompletion(_ result: Result, completion: @escaping CompletionResult) -> Void { switch result { case .success(let card): @@ -207,14 +207,16 @@ public class BackupService { completion(.failure(error.toTangemSdkError())) } } - + @discardableResult private func updateState() -> State { if repo.data.accessCode == nil || repo.data.primaryCard == nil || repo.data.backupCards.isEmpty { currentState = .preparing - } else if repo.data.attestSignature == nil || repo.data.backupData.count < repo.data.backupCards.count { + } else if repo.data.attestSignature == nil + || repo.data.backupData.count < repo.data.backupCards.count + || repo.data.primaryCardFinalized == false { currentState = .finalizingPrimaryCard } else if repo.data.finalizedBackupCardsCount < repo.data.backupCards.count { currentState = .finalizingBackupCard(index: repo.data.finalizedBackupCardsCount + 1) @@ -222,10 +224,10 @@ public class BackupService { currentState = .finished onBackupCompleted() } - + return currentState } - + private func addBackupCard(_ backupCardResponse: StartBackupCardLinkingTaskResponse, completion: @escaping CompletionResult) { let backupCard = backupCardResponse.backupCard @@ -237,7 +239,7 @@ public class BackupService { cardPublicKey: backupCard.cardPublicKey, firmwareVersion: backupCard.firmwareVersion) { [weak self] result in guard let self else { return } - + switch result { case .success(let certificate): var backupCard = backupCard @@ -250,19 +252,19 @@ public class BackupService { } } } - + private func readBackupCard(_ primaryCard: PrimaryCard, completion: @escaping CompletionResult) { let command = StartBackupCardLinkingTask(primaryCard: primaryCard, addedBackupCards: repo.data.backupCards.map { $0.cardId }, skipCompatibilityChecks: skipCompatibilityChecks) currentCommand = command - + sdk.startSession(with: command, filter: nil, initialMessage: Message(header: nil, body: "backup_add_backup_card_message".localized)) {[weak self] result in guard let self = self else { return } - + switch result { case .success(let response): self.addBackupCard(response, completion: completion) @@ -272,7 +274,7 @@ public class BackupService { self.currentCommand = nil } } - + private func handleFinalizePrimaryCard(completion: @escaping CompletionResult) { do { if handleErrors { @@ -280,53 +282,64 @@ public class BackupService { throw TangemSdkError.accessCodeOrPasscodeRequired } } - + let accessCode = repo.data.accessCode ?? UserCodeType.accessCode.defaultValue.sha256() let passcode = repo.data.passcode ?? UserCodeType.passcode.defaultValue.sha256() - + guard let primaryCard = repo.data.primaryCard else { throw TangemSdkError.missingPrimaryCard } - + if handleErrors { guard !repo.data.backupCards.isEmpty else { throw TangemSdkError.emptyBackupCards } - + guard repo.data.backupCards.count < 3 else { throw TangemSdkError.tooMuchBackupCards } } - - let task = FinalizePrimaryCardTask(backupCards: repo.data.backupCards, - accessCode: accessCode, - passcode: passcode, - readBackupStartIndex: repo.data.backupData.count, - attestSignature: repo.data.attestSignature, - onLink: { self.repo.data.attestSignature = $0 }, - onRead: { self.repo.data.backupData[$0.0] = $0.1 }) - + + let task = FinalizePrimaryCardTask( + backupCards: repo.data.backupCards, + accessCode: accessCode, + passcode: passcode, + readBackupStartIndex: repo.data.backupData.count, + attestSignature: repo.data.attestSignature, + onLink: { + self.repo.data.attestSignature = $0 + self.repo.data.primaryCardFinalized = false + }, + onRead: { + self.repo.data.backupData[$0.0] = $0.1 + self.repo.data.primaryCardFinalized = false + }, + onFinalize: { + self.repo.data.primaryCardFinalized = true + } + ) + let formattedCardId = CardIdFormatter(style: sdk.config.cardIdDisplayFormat).string(from: primaryCard.cardId) - + let initialMessage = formattedCardId.map { Message(header: nil, body: "backup_finalize_primary_card_message_format".localized($0)) } - + currentCommand = task - + sdk.startSession(with: task, cardId: primaryCard.cardId, initialMessage: initialMessage) {[weak self] result in completion(result) self?.currentCommand = nil } - + } catch { completion(.failure(error.toTangemSdkError())) } } - + private func handleWriteBackupCard(index: Int, completion: @escaping CompletionResult) { do { if handleErrors { @@ -334,57 +347,57 @@ public class BackupService { throw TangemSdkError.accessCodeOrPasscodeRequired } } - + let accessCode = repo.data.accessCode ?? UserCodeType.accessCode.defaultValue.sha256() let passcode = repo.data.passcode ?? UserCodeType.passcode.defaultValue.sha256() - + guard let attestSignature = repo.data.attestSignature else { throw TangemSdkError.missingPrimaryAttestSignature } - + guard let primaryCard = repo.data.primaryCard else { throw TangemSdkError.missingPrimaryCard } - + let cardIndex = index - 1 - + guard cardIndex < repo.data.backupCards.count else { throw TangemSdkError.noBackupCardForIndex } - + if handleErrors { guard repo.data.backupCards.count < 3 else { throw TangemSdkError.tooMuchBackupCards } } - + let backupCard = repo.data.backupCards[cardIndex] - + guard let backupData = repo.data.backupData[backupCard.cardId] else { throw TangemSdkError.noBackupDataForCard } - + let command = FinalizeBackupCardTask(primaryCard: primaryCard, backupCards: repo.data.backupCards, backupData: backupData, attestSignature: attestSignature, accessCode: accessCode, passcode: passcode) - + let formattedCardId = CardIdFormatter(style: sdk.config.cardIdDisplayFormat).string(from: backupCard.cardId) - + let initialMessage = formattedCardId.map { Message(header: nil, body: "backup_finalize_backup_card_message_format".localized($0)) } - + currentCommand = command - + sdk.startSession(with: command, cardId: backupCard.cardId, initialMessage: initialMessage) {[weak self] result in guard let self = self else { return } - + switch result { case .success(let card): self.repo.data.finalizedBackupCardsCount += 1 @@ -392,19 +405,19 @@ public class BackupService { case .failure(let error): completion(.failure(error)) } - + self.currentCommand = nil } - + } catch { completion(.failure(error.toTangemSdkError())) } } - + private func onBackupCompleted() { repo.reset() } - + private func fetchCertificate( for cardId: String, cardPublicKey: Data, @@ -441,7 +454,7 @@ public struct PrimaryCard: Codable { public let cardId: String public let cardPublicKey: Data public let linkingKey: Data - + //For compatibility check with backup card public let existingWalletsCount: Int public let isHDWalletAllowed: Bool @@ -450,7 +463,7 @@ public struct PrimaryCard: Codable { public let batchId: String? // Optional for compatibility with interrupted backups public let firmwareVersion: FirmwareVersion? // Optional for compatibility with interrupted backups public let isKeysImportAllowed: Bool? // Optional for compatibility with interrupted backups - + var certificate: Data? } @@ -461,7 +474,7 @@ struct BackupCard: Codable { let linkingKey: Data let attestSignature: Data let firmwareVersion: FirmwareVersion? // Optional for compatibility with interrupted backups - + var certificate: Data? } @@ -479,7 +492,8 @@ struct BackupServiceData: Codable { var backupCards: [BackupCard] = [] var backupData: [String:[EncryptedBackupData]] = [:] var finalizedBackupCardsCount: Int = 0 - + var primaryCardFinalized: Bool? = nil + var shouldSave: Bool { attestSignature != nil || !backupData.isEmpty } @@ -491,7 +505,7 @@ struct BackupServiceData: Codable { class BackupRepo { private let storage = SecureStorage() private var isFetching: Bool = false - + var data: BackupServiceData = .init() { didSet { do { @@ -503,7 +517,7 @@ class BackupRepo { Log.debug("BackupRepo updated") } } - + init () { do { try fetch() @@ -511,7 +525,7 @@ class BackupRepo { Log.debug(error) } } - + func reset() { do { try storage.delete(.backupData) @@ -520,18 +534,18 @@ class BackupRepo { } data = .init() } - + private func save() throws { guard !isFetching && data.shouldSave else { return } - + let encoded = try JSONEncoder().encode(data) try storage.store(encoded, forKey: .backupData) } - + private func fetch() throws { self.isFetching = true defer { self.isFetching = false } - + if let savedData = try storage.get(.backupData) { self.data = try JSONDecoder().decode(BackupServiceData.self, from: savedData) } diff --git a/TangemSdk/TangemSdk/Operations/Backup/FinalizePrimaryCardTask.swift b/TangemSdk/TangemSdk/Operations/Backup/FinalizePrimaryCardTask.swift index 8bb0cd0e..50e0edff 100644 --- a/TangemSdk/TangemSdk/Operations/Backup/FinalizePrimaryCardTask.swift +++ b/TangemSdk/TangemSdk/Operations/Backup/FinalizePrimaryCardTask.swift @@ -18,6 +18,7 @@ class FinalizePrimaryCardTask: CardSessionRunnable { private var attestSignature: Data? //We already have attestSignature private let onLink: (Data) -> Void private let onRead: ((String, [EncryptedBackupData])) -> Void + private let onFinalize: () -> Void private let readBackupStartIndex: Int init(backupCards: [BackupCard], @@ -26,16 +27,18 @@ class FinalizePrimaryCardTask: CardSessionRunnable { readBackupStartIndex: Int, //for restore attestSignature: Data?, onLink: @escaping (Data) -> Void, - onRead: @escaping ((String,[EncryptedBackupData])) -> Void) { + onRead: @escaping ((String,[EncryptedBackupData])) -> Void, + onFinalize: @escaping () -> Void) { self.backupCards = backupCards self.accessCode = accessCode self.passcode = passcode self.attestSignature = attestSignature self.onLink = onLink self.onRead = onRead + self.onFinalize = onFinalize self.readBackupStartIndex = readBackupStartIndex } - + deinit { Log.debug("FinalizePrimaryCardTask deinit") } @@ -108,6 +111,7 @@ class FinalizePrimaryCardTask: CardSessionRunnable { } guard card.firmwareVersion >= .keysImportAvailable else { + onFinalize() completion(.success(card)) return } @@ -117,8 +121,17 @@ class FinalizePrimaryCardTask: CardSessionRunnable { command?.run(in: session) { result in switch result { case .success: + self.onFinalize() completion(.success(card)) case .failure(let error): + // Backup data already finalized, but we didn't catch the original response due to NFC errors or tag lost. Just cover invalid state error + if case .invalidState = error { + Log.debug("Got \(error). Ignoring..") + self.onFinalize() + completion(.success(card)) + return + } + completion(.failure(error)) }