From de6faff381adbc1e5a9e0b29d829a9da90cef164 Mon Sep 17 00:00:00 2001 From: Markus Tamm Date: Thu, 12 Feb 2026 14:26:43 +0200 Subject: [PATCH] Add MyEID NFC functionality Refactor signing and decryption NFC operations --- .../Sources/CommonsLib/Constants.swift | 1 + .../CardActions/CardCommandsInternal.swift | 2 +- .../IdCardLib/CardActions/CardReaderNFC.swift | 54 ++++- .../IdCardLib/CardActions/Thales.swift | 1 + .../Operations/OperationUnblockPin.swift | 104 --------- RIADigiDoc.xcodeproj/project.pbxproj | 15 +- RIADigiDoc/DI/AppContainer.swift | 23 +- .../Error/Encryption/EncryptedDataError.swift | 40 ++++ .../Model/Error/NFC/ChangePinError.swift | 39 ++++ .../Model/Error/{ => NFC}/DecryptError.swift | 0 .../{ => NFC}/ReadCertAndSignError.swift | 0 .../Model/Error/NFC/UnblockPINError.swift | 39 ++++ RIADigiDoc/Domain/Model/KeychainKey.swift | 1 + RIADigiDoc/Domain/Model/NFC/NFCCardData.swift | 29 +++ .../Navigation/NavigationDestination.swift | 6 +- RIADigiDoc/Domain/NFC/NFCOperationBase.swift | 118 ++++++++++ .../Domain/NFC/NFCSessionStringsUtil.swift | 94 ++++++++ .../Domain/NFC/OperationChangePin.swift | 132 +++++++++++ RIADigiDoc/Domain/NFC/OperationDecrypt.swift | 89 ++------ .../Domain/NFC/OperationReadCardData.swift | 141 ++++++++++++ .../Domain/NFC/OperationReadCertAndSign.swift | 88 ++------ .../Domain/NFC/OperationUnblockPin.swift | 131 +++++++++++ RIADigiDoc/Domain/Preferences/DataStore.swift | 19 +- .../Preferences/DataStoreProtocol.swift | 4 +- .../Container/Signing/IdCard/IdCardView.swift | 5 +- .../Container/Signing/NFC/NFCInputView.swift | 35 +-- .../Container/Signing/NFC/NFCView.swift | 124 +++++------ .../Component/My eID/MyEidPinChangeView.swift | 15 +- .../UI/Component/My eID/MyEidView.swift | 26 ++- .../Navigation/NavigationDestinations.swift | 13 +- .../EncryptedData/EncryptedDataUtil.swift | 99 +++++++++ .../MyEid/MyEidPinChangeViewModel.swift | 110 ++++++--- .../MyEid/Shared/SharedMyEidSession.swift | 12 + .../Shared/SharedMyEidSessionProtocol.swift | 2 + .../MyEidPinChangeViewModelProtocol.swift | 2 +- .../ViewModel/Signing/NFC/NFCViewModel.swift | 208 ++++++++++++++++-- .../Signing/NFC/NFCViewModelProtocol.swift | 8 +- 37 files changed, 1395 insertions(+), 434 deletions(-) delete mode 100644 Modules/IdCardLib/Sources/IdCardLib/Operations/OperationUnblockPin.swift create mode 100644 RIADigiDoc/Domain/Model/Error/Encryption/EncryptedDataError.swift create mode 100644 RIADigiDoc/Domain/Model/Error/NFC/ChangePinError.swift rename RIADigiDoc/Domain/Model/Error/{ => NFC}/DecryptError.swift (100%) rename RIADigiDoc/Domain/Model/Error/{ => NFC}/ReadCertAndSignError.swift (100%) create mode 100644 RIADigiDoc/Domain/Model/Error/NFC/UnblockPINError.swift create mode 100644 RIADigiDoc/Domain/Model/NFC/NFCCardData.swift create mode 100644 RIADigiDoc/Domain/NFC/NFCOperationBase.swift create mode 100644 RIADigiDoc/Domain/NFC/NFCSessionStringsUtil.swift create mode 100644 RIADigiDoc/Domain/NFC/OperationChangePin.swift create mode 100644 RIADigiDoc/Domain/NFC/OperationReadCardData.swift create mode 100644 RIADigiDoc/Domain/NFC/OperationUnblockPin.swift create mode 100644 RIADigiDoc/Util/EncryptedData/EncryptedDataUtil.swift diff --git a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift index 22d23fa1..dc1dd11b 100644 --- a/Modules/CommonsLib/Sources/CommonsLib/Constants.swift +++ b/Modules/CommonsLib/Sources/CommonsLib/Constants.swift @@ -100,6 +100,7 @@ public struct Constants { public struct File { public static let LibDigidocLog = "libdigidocpp.log" public static let LDAPCertsPem = "ldapCerts.pem" + public static let nfcCANKey = "canKey.txt" } public struct FileBaseName { diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommandsInternal.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommandsInternal.swift index 8f9fc1b7..4159897f 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommandsInternal.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardCommandsInternal.swift @@ -74,7 +74,7 @@ extension CardCommandsInternal { return case 0x6A80: // New pin is invalid throw IdCardInternalError.invalidNewPin - case 0x63C0, 0x6983: // Authentication method blocked + case 0x63C0, 0x6983, 0x6984: // Authentication method blocked throw IdCardInternalError.pinVerificationFailed // For pin codes this means verification failed due to wrong pin case let uInt16 where (uInt16 & 0xFFF0) == 0x63C0: diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift index e7d7f4e5..a5fcbe76 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/CardReaderNFC.swift @@ -234,19 +234,38 @@ class CardReaderNFC: @unchecked CardReader, Loggable { } return (tlvEnc, tlvRes, tlvMac) } - + // swiftlint:disable cyclomatic_complexity func transmit(_ apduData: Bytes) async throws -> (responseData: Bytes, sw: UInt16) { CardReaderNFC.logger().info("Plain >: \(apduData.toHex)") guard let apdu = NFCISO7816APDU(data: Data(apduData)) else { throw IdCardInternalError.invalidAPDU } _ = SSC.increment() - let DO87 = try getDO87(apdu) - let DO97 = try getDO97(apdu) + let DO87: Data + if let data = apdu.data, !data.isEmpty { + let ivValue = try AES.CBC(key: ksEnc).encrypt(SSC) + let encData = try AES.CBC(key: ksEnc, ivVal: ivValue).encrypt(data.addPadding()) + if apdu.instructionCode & 0x01 == 0 { + DO87 = TLV(tag: 0x87, bytes: [0x01] + encData).data + } else { + DO87 = TLV(tag: 0x85, bytes: encData).data + } + } else { + DO87 = Data() + } + let DO97: Data + if apdu.expectedResponseLength > 0 { + DO97 = TLV( + tag: 0x97, + bytes: [UInt8(apdu.expectedResponseLength == 256 ? 0 : apdu.expectedResponseLength)] + ).data + } else { + DO97 = Data() + } let cmdHeader: Bytes = [apdu.instructionClass | 0x0C, apdu.instructionCode, apdu.p1Parameter, apdu.p2Parameter] - let MValue = cmdHeader.addPadding() + DO87 + DO97 - let NValue = SSC + MValue - let mac = try AES.CMAC(key: ksMac).authenticate(bytes: NValue.addPadding()) + let mVal = cmdHeader.addPadding() + DO87 + DO97 + let nVal = SSC + mVal + let mac = try AES.CMAC(key: ksMac).authenticate(bytes: nVal.addPadding()) let DO8E = TLV(tag: 0x8E, bytes: mac).data let send = DO87 + DO97 + DO8E let response = try await tag.sendCommand( @@ -257,15 +276,25 @@ class CardReaderNFC: @unchecked CardReader, Loggable { data: send, leByte: 256 ) - let (tlvEnc, tlvRes, tlvMac) = try getTLVs(response) + var tlvEnc: TKTLVRecord? + var tlvRes: TKTLVRecord? + var tlvMac: TKTLVRecord? + for tlv in TLV.sequenceOfRecords(from: response) ?? [] { + switch tlv.tag { + case 0x85, 0x87: tlvEnc = tlv + case 0x99: tlvRes = tlv + case 0x8E: tlvMac = tlv + default: print("Unknown tag") + } + } guard let tlvRes else { throw IdCardInternalError.missingRESTag } guard let tlvMac else { throw IdCardInternalError.missingMACTag } - let KValue = SSC.increment() + (tlvEnc?.data ?? Data()) + tlvRes.data - if try Data(AES.CMAC(key: ksMac).authenticate(bytes: KValue.addPadding())) != tlvMac.value { + let kVal = SSC.increment() + (tlvEnc?.data ?? Data()) + tlvRes.data + if try Data(AES.CMAC(key: ksMac).authenticate(bytes: kVal.addPadding())) != tlvMac.value { throw IdCardInternalError.invalidMACValue } guard let tlvEnc else { @@ -273,10 +302,13 @@ class CardReaderNFC: @unchecked CardReader, Loggable { return (.init(), UInt16(tlvRes.value[0], tlvRes.value[1])) } let ivValue = try AES.CBC(key: ksEnc).encrypt(SSC) - let responseData = try (try AES.CBC(key: ksEnc, ivVal: ivValue).decrypt(tlvEnc.value[1...])).removePadding() - CardReaderNFC.logger().info("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)") + let responseData = try (try AES.CBC(key: ksEnc, ivVal: ivValue) + .decrypt(tlvEnc.tag == 0x85 ? tlvEnc.value : tlvEnc.value[1...])) + .removePadding() + CardReaderNFC.logger().debug("Plain <: \(responseData.toHex) \(tlvRes.value.toHex)") return (Bytes(responseData), UInt16(tlvRes.value[0], tlvRes.value[1])) } + // swiftlint:enable cyclomatic_complexity // MARK: - Utils diff --git a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift index 2341b2af..4e09b5ce 100644 --- a/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift +++ b/Modules/IdCardLib/Sources/IdCardLib/CardActions/Thales.swift @@ -115,6 +115,7 @@ final class Thales: CardCommandsInternal { guard type != .puk else { throw IdCardInternalError.notSupportedCodeType } + _ = try await select(file: Thales.kAID) try await unblockCode(type.pinRef, puk: puk, newCode: newCode) } diff --git a/Modules/IdCardLib/Sources/IdCardLib/Operations/OperationUnblockPin.swift b/Modules/IdCardLib/Sources/IdCardLib/Operations/OperationUnblockPin.swift deleted file mode 100644 index 26a45b50..00000000 --- a/Modules/IdCardLib/Sources/IdCardLib/Operations/OperationUnblockPin.swift +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2017 - 2025 Riigi Infosüsteemi Amet - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - * - */ - -import Foundation -import CoreNFC -import CommonCrypto -import CryptoTokenKit -internal import SwiftECC -import BigInt -import Security - -public enum UnblockPINError: Error { - case missingRequiredParameter - case failed - case general -} - -@MainActor -public class OperationUnblockPin: NSObject { - private var session: NFCTagReaderSession? - private var CAN: String = "" - private var codeType: CodeType? - private var puk: SecureData? - private var newPin: SecureData? - private let nfcMessage: String = "Please place your ID card against the smart device" - private let connection = NFCConnection() - private var continuation: CheckedContinuation? - - public func startReading(CAN: String, codeType: CodeType, puk: SecureData, newPin: SecureData) async throws { - - return try await withCheckedThrowingContinuation { continuation in - self.continuation = continuation - - guard NFCTagReaderSession.readingAvailable else { - continuation.resume(throwing: IdCardInternalError.nfcNotSupported) - return - } - - self.CAN = CAN - self.codeType = codeType - self.puk = puk - self.newPin = newPin - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) - session?.alertMessage = nfcMessage - session?.begin() - } - } -} - -extension OperationUnblockPin: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - Task { @MainActor [weak self] in - guard let self else { return } - defer { - self.session = nil - } - - guard let codeType = self.codeType, let puk = self.puk, let newPin = self.newPin else { - self.continuation?.resume(throwing: UnblockPINError.missingRequiredParameter) - session.invalidate(errorMessage: "PIN change failed") - return - } - do { - session.alertMessage = "Hold your ID card against your smart device until the data is read" - let tag = try await self.connection.setup(session, tags: tags) - let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.CAN) - do { - try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin) - } catch { - throw UnblockPINError.failed - } - - self.continuation?.resume(with: .success(())) - session.alertMessage = "PIN changed" - session.invalidate() - } catch { - session.invalidate(errorMessage: "PIN change failed") - self.continuation?.resume(throwing: error) - } - } - } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError _: Error) { - self.session = nil - } -} diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index a7221d9b..57e56b55 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -160,10 +160,13 @@ Domain/Model/EncryptionServerInfo.swift, Domain/Model/EncryptionServerOption.swift, Domain/Model/EncryptionServerOptionId.swift, - Domain/Model/Error/DecryptError.swift, + Domain/Model/Error/Encryption/EncryptedDataError.swift, Domain/Model/Error/FileOpeningErrors.swift, "Domain/Model/Error/My eID/MyEidCodeChangeError.swift", - Domain/Model/Error/ReadCertAndSignError.swift, + Domain/Model/Error/NFC/ChangePinError.swift, + Domain/Model/Error/NFC/DecryptError.swift, + Domain/Model/Error/NFC/ReadCertAndSignError.swift, + Domain/Model/Error/NFC/UnblockPINError.swift, Domain/Model/FileItem.swift, Domain/Model/IdCard/IdCardData.swift, Domain/Model/IdCard/RetryCount.swift, @@ -171,8 +174,10 @@ "Domain/Model/My eID/MyEidDocumentStatus.swift", "Domain/Model/My eID/MyEidPinCodeAction.swift", "Domain/Model/My eID/MyEidPinCodeStep.swift", + Domain/Model/NFC/NFCCardData.swift, Domain/Model/Notification/ContainerNotificationType.swift, Domain/Model/SettingsMenuBottomSheetPages.swift, + Domain/Model/Signing/ActionType.swift, Domain/Model/Signing/MobileId/MobileIdInputData.swift, Domain/Model/Signing/NFC/NFCInputData.swift, Domain/Model/Signing/SigningMethod.swift, @@ -182,9 +187,14 @@ Domain/Model/SupportedLanguage.swift, Domain/Model/SupportedTheme.swift, Domain/Model/ToastMessage.swift, + Domain/NFC/NFCOperationBase.swift, Domain/NFC/NFCSessionStrings.swift, + Domain/NFC/NFCSessionStringsUtil.swift, + Domain/NFC/OperationChangePin.swift, Domain/NFC/OperationDecrypt.swift, + Domain/NFC/OperationReadCardData.swift, Domain/NFC/OperationReadCertAndSign.swift, + Domain/NFC/OperationUnblockPin.swift, Domain/Preferences/DataStore.swift, Domain/Preferences/DataStoreProtocol.swift, Domain/Preferences/KeychainStore.swift, @@ -211,6 +221,7 @@ UI/Theme/Theme.swift, Util/Certificate/CertificateUtil.swift, Util/Certificate/CertificateUtilProtocol.swift, + Util/EncryptedData/EncryptedDataUtil.swift, Util/File/FileInspector.swift, Util/File/FileInspectorProtocol.swift, Util/Language/LanguageSettings.swift, diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index 943a1c28..edf540a7 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -509,7 +509,10 @@ extension Container { self { @MainActor in NFCViewModel( dataStore: self.dataStore(), - userAgentUtil: self.userAgentUtil() + userAgentUtil: self.userAgentUtil(), + certificateUtil: self.certificateUtil(), + sharedMyEidSession: self.sharedMyEidSession(), + keychainStore: self.keychainStore() ) } } @@ -523,19 +526,33 @@ extension Container { } } + // swiftlint:disable closure_parameter_position + // swiftlint:disable large_tuple @MainActor - var myEidPinChangeViewModel: ParameterFactory<(MyEidPinCodeAction, CodeType, String), MyEidPinChangeViewModel> { - self { @MainActor (pinAction: MyEidPinCodeAction, codeType: CodeType, personalCode: String + var myEidPinChangeViewModel: ParameterFactory<( + MyEidPinCodeAction, + CodeType, + String, + ActionMethod + ), MyEidPinChangeViewModel> { + self { @MainActor ( + pinAction: MyEidPinCodeAction, + codeType: CodeType, + personalCode: String, + actionMethod: ActionMethod ) -> MyEidPinChangeViewModel in MyEidPinChangeViewModel( pinAction: pinAction, codeType: codeType, personalCode: personalCode, + actionMethod: actionMethod, idCardRepository: self.idCardRepository(), sharedMyEidSession: self.sharedMyEidSession() ) } } + // swiftlint:enable closure_parameter_position + // swiftlint:enable large_tuple @MainActor var crashReportManager: Factory { diff --git a/RIADigiDoc/Domain/Model/Error/Encryption/EncryptedDataError.swift b/RIADigiDoc/Domain/Model/Error/Encryption/EncryptedDataError.swift new file mode 100644 index 00000000..9e766304 --- /dev/null +++ b/RIADigiDoc/Domain/Model/Error/Encryption/EncryptedDataError.swift @@ -0,0 +1,40 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +public enum EncryptedDataError: Error, LocalizedError { + case unableToLocateAppSupportDirectory + case keyFileDoesNotExist + case encryptionFailed + case decryptionFailed + + public var errorDescription: String? { + switch self { + case .unableToLocateAppSupportDirectory: + return "Unable to locate Application Support directory" + case .keyFileDoesNotExist: + return "Encryption key file does not exist" + case .encryptionFailed: + return "Failed to encrypt data" + case .decryptionFailed: + return "Failed to decrypt data" + } + } +} diff --git a/RIADigiDoc/Domain/Model/Error/NFC/ChangePinError.swift b/RIADigiDoc/Domain/Model/Error/NFC/ChangePinError.swift new file mode 100644 index 00000000..f362f703 --- /dev/null +++ b/RIADigiDoc/Domain/Model/Error/NFC/ChangePinError.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +public enum ChangePinError: Error { + case missingRequiredParameter + case cancelled + case unknown(Error) +} + +extension ChangePinError: LocalizedError { + public var errorDescription: String? { + switch self { + case .missingRequiredParameter: + return "Missing required parameter" + case .cancelled: + return "Operation cancelled by user" + case .unknown(let error): + return "Unknown error: \(error.localizedDescription)" + } + } +} diff --git a/RIADigiDoc/Domain/Model/Error/DecryptError.swift b/RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift similarity index 100% rename from RIADigiDoc/Domain/Model/Error/DecryptError.swift rename to RIADigiDoc/Domain/Model/Error/NFC/DecryptError.swift diff --git a/RIADigiDoc/Domain/Model/Error/ReadCertAndSignError.swift b/RIADigiDoc/Domain/Model/Error/NFC/ReadCertAndSignError.swift similarity index 100% rename from RIADigiDoc/Domain/Model/Error/ReadCertAndSignError.swift rename to RIADigiDoc/Domain/Model/Error/NFC/ReadCertAndSignError.swift diff --git a/RIADigiDoc/Domain/Model/Error/NFC/UnblockPINError.swift b/RIADigiDoc/Domain/Model/Error/NFC/UnblockPINError.swift new file mode 100644 index 00000000..0dae14a8 --- /dev/null +++ b/RIADigiDoc/Domain/Model/Error/NFC/UnblockPINError.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +public enum UnblockPINError: Error { + case missingRequiredParameter + case cancelled + case unknown(Error) +} + +extension UnblockPINError: LocalizedError { + public var errorDescription: String? { + switch self { + case .missingRequiredParameter: + return "Missing required parameter" + case .cancelled: + return "Operation cancelled by user" + case .unknown(let error): + return "Unknown error: \(error.localizedDescription)" + } + } +} diff --git a/RIADigiDoc/Domain/Model/KeychainKey.swift b/RIADigiDoc/Domain/Model/KeychainKey.swift index b630585e..ce497291 100644 --- a/RIADigiDoc/Domain/Model/KeychainKey.swift +++ b/RIADigiDoc/Domain/Model/KeychainKey.swift @@ -19,4 +19,5 @@ public enum KeychainKey: String, CaseIterable, Sendable { case proxyPassword = "proxy_password" + case nfcCANKey = "nfc_can_key" } diff --git a/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift b/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift new file mode 100644 index 00000000..912eaf36 --- /dev/null +++ b/RIADigiDoc/Domain/Model/NFC/NFCCardData.swift @@ -0,0 +1,29 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import IdCardLib + +public struct NFCCardData: Sendable { + let publicData: CardInfo + let authenticationCertificate: Data? + let signatureCertificate: Data? + let retryCount: RetryCount + let isPUKChangable: Bool +} diff --git a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift index 3956a7d9..cdcf216d 100644 --- a/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift +++ b/RIADigiDoc/Domain/Model/Navigation/NavigationDestination.swift @@ -72,12 +72,14 @@ public enum NavigationDestination: Hashable { case myEidRootView case myEidView( - idCardData: IdCardData + idCardData: IdCardData, + actionMethod: ActionMethod ) case myEidPinView( pinAction: MyEidPinCodeAction, codeType: CodeType, - personalCode: String + personalCode: String, + actionMethod: ActionMethod ) } diff --git a/RIADigiDoc/Domain/NFC/NFCOperationBase.swift b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift new file mode 100644 index 00000000..347da08b --- /dev/null +++ b/RIADigiDoc/Domain/NFC/NFCOperationBase.swift @@ -0,0 +1,118 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CoreNFC +import IdCardLib +import UtilsLib + +@MainActor +public class NFCOperationBase: NSObject, Loggable, @MainActor NFCTagReaderSessionDelegate { + var session: NFCTagReaderSession? + var isFinished = false + var canNumber: String = "" + var nfcError: String = "" + var strings: NFCSessionStrings? + + let connection = NFCConnection() + + func updateAlertMessage(step: Int) { + let stepMessages = [ + strings?.initialMessage ?? "", + strings?.step1Message ?? "", + strings?.step2Message ?? "", + strings?.step3Message ?? "", + strings?.step4Message ?? "" + ] + + let stepMessage = stepMessages[min(step, stepMessages.count - 1)] + let progressBar = ProgressBar(currentStep: step) + var message = stepMessage + Self.logger().info("NFC: Updating alert message to: \(message)") + message += "\n\n\(progressBar.generate())" + session?.alertMessage = message + } + + func success() { + session?.alertMessage = strings?.successMessage ?? "" + session?.invalidate() + } + + func handleIdCardError(_ error: IdCardError) { + Self.logger().error("NFC: Handling IdCardError: \(error)") + + switch error { + case .wrongCAN: + nfcError = strings?.canErrorMessage ?? "" + case .wrongPIN(let triesLeft): + if triesLeft > 1 { + nfcError = strings?.pinWrongMultipleErrorMessage ?? "" + } else if triesLeft == 1 { + nfcError = strings?.pinWrongErrorMessage ?? "" + } else { + nfcError = strings?.pinBlockedErrorMessage ?? "" + } + case .sessionError: + nfcError = strings?.sessionErrorMessage ?? "" + default: + nfcError = strings?.technicalErrorMessage ?? "" + } + } + + func handleIdCardInternalError( + _ error: IdCardInternalError, + session: NFCTagReaderSession + ) { + let idCardError = error.getIdCardError() + Self.logger().error("NFC: IdCardError detected: \(idCardError)") + handleIdCardError(idCardError) + session.invalidate(errorMessage: nfcError ?? "") + } + + func handleUnknownError( + _ error: Error, + session: NFCTagReaderSession + ) -> Error { + Self.logger().error("NFC: Unknown error type: \(type(of: error))") + Self.logger().error("NFC: Error details: \(error.localizedDescription)") + session.invalidate(errorMessage: strings?.sessionErrorMessage ?? "") + return error + } + + func checkIfFinished(error: Error) -> Bool { + guard !isFinished else { + Self.logger().info("NFC: Operation already finished, ignoring error: \(error.localizedDescription)") + return true + } + isFinished = true + return false + } + + // MARK: - NFCTagReaderSessionDelegate + + public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } + + public func tagReaderSession(_: NFCTagReaderSession, didDetect _: [NFCTag]) { + // Override in subclasses + } + + public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError _: Error) { + // Override in subclasses + } +} diff --git a/RIADigiDoc/Domain/NFC/NFCSessionStringsUtil.swift b/RIADigiDoc/Domain/NFC/NFCSessionStringsUtil.swift new file mode 100644 index 00000000..afd13b5b --- /dev/null +++ b/RIADigiDoc/Domain/NFC/NFCSessionStringsUtil.swift @@ -0,0 +1,94 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation + +public struct NFCSessionStringsUtil { + private let localize: (String, [String]) -> String + + public init(localize: @escaping (String, [String]) -> String) { + self.localize = localize + } + + public func makeDefault(pinName: String = "") -> NFCSessionStrings { + customLocalizations(pinName: pinName) + } + + public func makeForDecrypt(pinName: String) -> NFCSessionStrings { + customLocalizations( + pinName: pinName, + step4Message: localize("Decrypting in progress", []) + ) + } + + public func makeForSigning(pinName: String) -> NFCSessionStrings { + customLocalizations( + pinName: pinName, + step4Message: localize("Signing in progress", []), + successMessage: localize("Signature added", []) + ) + } + + public func makeForUnblock(pinName: String) -> NFCSessionStrings { + customLocalizations( + pinName: pinName, + successMessage: localize("PIN unblocked", [pinName]) + ) + } + + public func makeForChangePin(pinName: String) -> NFCSessionStrings { + customLocalizations( + pinName: pinName, + successMessage: localize("PIN changed", [pinName]) + ) + } + + public func customLocalizations( + pinName: String, + initialMessage: String? = nil, + step1Message: String? = nil, + step2Message: String? = nil, + step3Message: String? = nil, + step4Message: String? = nil, + successMessage: String? = nil, + canErrorMessage: String? = nil, + pinWrongMultipleErrorMessage: String? = nil, + pinWrongErrorMessage: String? = nil, + pinBlockedErrorMessage: String? = nil, + technicalErrorMessage: String? = nil, + sessionErrorMessage: String? = nil + ) -> NFCSessionStrings { + NFCSessionStrings( + initialMessage: initialMessage ?? localize("Please place your ID card against the smart device", []), + step1Message: step1Message ?? localize( + "Hold your ID card against your smart device until the data is read", []), + step2Message: step2Message ?? localize("Reading data", []), + step3Message: step3Message ?? localize("Reading certificate", []), + step4Message: step4Message ?? localize("Reading data", []), + successMessage: successMessage ?? localize("Data read", []), + canErrorMessage: canErrorMessage ?? localize("Wrong CAN", []), + pinWrongMultipleErrorMessage: pinWrongMultipleErrorMessage ?? localize( + "PIN verification error multiple", [pinName, "2"]), + pinWrongErrorMessage: pinWrongErrorMessage ?? localize("PIN verification error one", [pinName]), + pinBlockedErrorMessage: pinBlockedErrorMessage ?? localize("PIN blocked", [pinName]), + technicalErrorMessage: technicalErrorMessage ?? localize("NFC technical error", []), + sessionErrorMessage: sessionErrorMessage ?? localize("NFC session error", []) + ) + } +} diff --git a/RIADigiDoc/Domain/NFC/OperationChangePin.swift b/RIADigiDoc/Domain/NFC/OperationChangePin.swift new file mode 100644 index 00000000..201fc621 --- /dev/null +++ b/RIADigiDoc/Domain/NFC/OperationChangePin.swift @@ -0,0 +1,132 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CoreNFC +import UtilsLib +import IdCardLib + +@MainActor +public class OperationChangePin: NFCOperationBase { + private var codeType: CodeType? + private var currentPin: SecureData? + private var newPin: SecureData? + private var continuation: CheckedContinuation? + + public func startChanging( + canNumber: String, + codeType: CodeType, + currentPin: SecureData, + newPin: SecureData, + strings: NFCSessionStrings, + ) async throws { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + + guard NFCTagReaderSession.readingAvailable else { + continuation.resume(throwing: IdCardInternalError.nfcNotSupported) + return + } + + self.canNumber = canNumber + self.codeType = codeType + self.currentPin = currentPin + self.newPin = newPin + self.strings = strings + session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) + updateAlertMessage(step: 0) + session?.begin() + } + } + + // MARK: - NFCTagReaderSessionDelegate + + public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + Task { @MainActor in + defer { + self.session = nil + } + + guard let codeType = self.codeType, + let currentPin = self.currentPin, + let newPin = self.newPin else { + let error = ChangePinError.missingRequiredParameter + OperationChangePin.logger().error("NFC: \(error.localizedDescription)") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Missing required parameters") + continuation?.resume(throwing: error) + return + } + + do { + updateAlertMessage(step: 1) + OperationChangePin.logger().info("NFC: Setting up NFC connection for PIN change...") + let tag = try await self.connection.setup(session, tags: tags) + + updateAlertMessage(step: 2) + let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber) + + updateAlertMessage(step: 3) + OperationChangePin.logger().info("NFC: Changing \(codeType.name)...") + try await cardCommands.changeCode(codeType, to: newPin, verifyCode: currentPin) + OperationChangePin.logger().info("NFC: \(codeType.name) changed successfully") + + self.continuation?.resume(with: .success(())) + success() + } catch { + guard !checkIfFinished(error: error) else { return } + + if let idCardInternalError = error as? IdCardInternalError { + handleIdCardInternalError(idCardInternalError, session: session) + continuation?.resume(throwing: error) + return + } + + if let changePinError = error as? ChangePinError { + OperationChangePin.logger() + .error("NFC: changePinError: \(changePinError.localizedDescription)") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? "") + continuation?.resume(throwing: error) + return + } + + let wrappedError = UnblockPINError.unknown(handleUnknownError(error, session: session)) + continuation?.resume(throwing: wrappedError) + } + } + } + + public override func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { + self.session = nil + guard !isFinished else { return } + isFinished = true + if let nfcError = error as? NFCReaderError { + switch nfcError.code { + case .readerSessionInvalidationErrorUserCanceled: + continuation?.resume(throwing: IdCardInternalError.cancelledByUser) + return + + default: + break + } + } + continuation?.resume(throwing: error) + } + +} diff --git a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift index ad7c80a5..fc3eed18 100644 --- a/RIADigiDoc/Domain/NFC/OperationDecrypt.swift +++ b/RIADigiDoc/Domain/NFC/OperationDecrypt.swift @@ -30,19 +30,11 @@ import CryptoSwift import UtilsLib @MainActor -public class OperationDecrypt: NSObject, Loggable { - - private var session: NFCTagReaderSession? - private var isFinished = false +public class OperationDecrypt: NFCOperationBase { private var containerFile: URL? private var recipients: [Addressee] = [] - private var canNumber: String = "" private var pin1Number: SecureData = SecureData([0x00]) private var continuation: CheckedContinuation? - private var connection = NFCConnection() - - private var nfcError: String? = "" - private var strings: NFCSessionStrings? public func processDecrypt( canNumber: String, @@ -71,51 +63,9 @@ public class OperationDecrypt: NSObject, Loggable { } } - private func updateAlertMessage(step: Int) { - let stepMessages = [ - strings?.initialMessage ?? "", - strings?.step1Message ?? "", - strings?.step2Message ?? "", - strings?.step3Message ?? "", - strings?.step4Message ?? "" - ] - - let stepMessage = stepMessages[min(step, stepMessages.count - 1)] - let progressBar = ProgressBar(currentStep: step) - var message = stepMessage - OperationDecrypt.logger().info("NFC: Updating alert message to: \(message)") - message += "\n\n\(progressBar.generate())" - session?.alertMessage = message - } - - private func success() { - session?.alertMessage = strings?.successMessage ?? "" - session?.invalidate() - } - - private func handleIdCardError(_ error: IdCardError) { - switch error { - case .wrongCAN: - nfcError = strings?.canErrorMessage ?? "" - case .wrongPIN(let triesLeft): - if triesLeft > 1 { - nfcError = strings?.pinWrongMultipleErrorMessage ?? "" - } else if triesLeft == 1 { - nfcError = strings?.pinWrongErrorMessage ?? "" - } else { - nfcError = strings?.pinBlockedErrorMessage ?? "" - } - case .sessionError: - nfcError = strings?.sessionErrorMessage ?? "" - default: - nfcError = strings?.technicalErrorMessage ?? "" - } - } -} - -extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + // MARK: - NFCTagReaderSessionDelegate + public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { Task { @MainActor in defer { self.session = nil @@ -124,7 +74,8 @@ extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { guard let containerFile else { let error = DecryptError.containerFileInvalid OperationDecrypt.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "Failed to read container file") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to read container file") continuation?.resume(throwing: error) return } @@ -132,7 +83,8 @@ extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { if containerFile.path.isEmpty { let error = DecryptError.containerFileInvalid OperationDecrypt.logger().error("NFC: Container file path is empty") - session.invalidate(errorMessage: "Failed to read container file") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to read container file") continuation?.resume(throwing: error) return } @@ -140,7 +92,8 @@ extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { if recipients.isEmpty { let error = DecryptError.recipientsEmpty OperationDecrypt.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "No recipients found") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "No recipients found") continuation?.resume(throwing: error) return } @@ -165,42 +118,29 @@ extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { continuation?.resume(with: .success(decryptedContainer)) success() } catch { - guard !isFinished else { - OperationDecrypt.logger() - .info("NFC: Operation already finished, ignoring error: \(error.localizedDescription)") - return - } - isFinished = true + guard !checkIfFinished(error: error) else { return } if let idCardInternalError = error as? IdCardInternalError { - let idCardError = idCardInternalError.getIdCardError() - OperationDecrypt.logger().error("NFC: IdCardError detected: \(idCardError)") - handleIdCardError(idCardError) - session.invalidate(errorMessage: nfcError ?? "") + handleIdCardInternalError(idCardInternalError, session: session) continuation?.resume(throwing: error) return } if let decryptError = error as? DecryptError { OperationDecrypt.logger() - .error("NFC: ReadCertAndDecryptError: \(decryptError.localizedDescription)") + .error("NFC: DecryptError: \(decryptError.localizedDescription)") session.invalidate(errorMessage: strings?.technicalErrorMessage ?? "") continuation?.resume(throwing: error) return } - OperationDecrypt.logger().error("NFC: Unknown error type: \(type(of: error))") - OperationDecrypt.logger().error("NFC: Error details: \(error.localizedDescription)") - let wrappedError = DecryptError.unknown(error) - session.invalidate(errorMessage: strings?.sessionErrorMessage ?? "") + let wrappedError = DecryptError.unknown(handleUnknownError(error, session: session)) continuation?.resume(throwing: wrappedError) } } } - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { + public override func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { self.session = nil guard !isFinished else { return } isFinished = true @@ -216,4 +156,5 @@ extension OperationDecrypt: @MainActor NFCTagReaderSessionDelegate { } continuation?.resume(throwing: error) } + } diff --git a/RIADigiDoc/Domain/NFC/OperationReadCardData.swift b/RIADigiDoc/Domain/NFC/OperationReadCardData.swift new file mode 100644 index 00000000..ce2249a2 --- /dev/null +++ b/RIADigiDoc/Domain/NFC/OperationReadCardData.swift @@ -0,0 +1,141 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CoreNFC +import IdCardLib +import UtilsLib + +@MainActor +final public class OperationReadCardData: NFCOperationBase { + private var continuation: CheckedContinuation? + + public func startReading( + canNumber: String, + strings: NFCSessionStrings, + ) async throws -> NFCCardData { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + + guard NFCTagReaderSession.readingAvailable else { + continuation.resume(throwing: IdCardInternalError.nfcNotSupported) + return + } + + self.canNumber = canNumber + self.strings = strings + + session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) + updateAlertMessage(step: 0) + session?.begin() + } + } + + // MARK: - NFCTagReaderSessionDelegate + + public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + Task { + defer { + self.session = nil + } + + do { + updateAlertMessage(step: 1) + OperationReadCardData.logger().info("Setting up NFC connection...") + let tag = try await connection.setup(session, tags: tags) + + updateAlertMessage(step: 2) + OperationReadCardData.logger().info("Establishing secure channel with CAN...") + let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: canNumber) + + OperationReadCardData.logger().info("Reading public data...") + let cardInfo = try await cardCommands.readPublicData() + OperationReadCardData.logger().info("Public data read successfully") + + updateAlertMessage(step: 3) + OperationReadCardData.logger().info("Reading authentication certificate") + let authenticationCertificate = try await cardCommands.readAuthenticationCertificate() + + OperationReadCardData.logger().info("Reading signature certificate") + let signatureCertificate = try await cardCommands.readSignatureCertificate() + + updateAlertMessage(step: 4) + OperationReadCardData.logger().info("Reading PIN retry counts...") + + let pin1Count = try await cardCommands.readCodeTryCounterRecord(.pin1) + OperationReadCardData.logger().info("PIN1 retry count: \(pin1Count)") + + let pin2Count = try await cardCommands.readCodeTryCounterRecord(.pin2) + OperationReadCardData.logger().info("PIN2 retry count: \(pin2Count)") + + let pukCount = try await cardCommands.readCodeTryCounterRecord(.puk) + OperationReadCardData.logger().info("PUK retry count: \(pukCount)") + + let retryCount = RetryCount( + pin1: pin1Count, + pin2: pin2Count, + puk: pukCount + ) + + OperationReadCardData.logger().info("NFC: reading can change PUK") + let canChangePUK = cardCommands.canChangePUK + OperationReadCardData.logger().info("NFC: can change PUK: \(canChangePUK)") + + let cardData = NFCCardData( + publicData: cardInfo, + authenticationCertificate: authenticationCertificate, + signatureCertificate: signatureCertificate, + retryCount: retryCount, + isPUKChangable: canChangePUK + ) + + continuation?.resume(with: .success(cardData)) + success() + } catch { + guard !checkIfFinished(error: error) else { return } + + if let idCardInternalError = error as? IdCardInternalError { + handleIdCardInternalError(idCardInternalError, session: session) + continuation?.resume(throwing: error) + return + } + + let unknownError = handleUnknownError(error, session: session) + continuation?.resume(throwing: unknownError) + } + } + } + + public override func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { + self.session = nil + guard !isFinished else { return } + isFinished = true + if let nfcError = error as? NFCReaderError { + switch nfcError.code { + case .readerSessionInvalidationErrorUserCanceled: + continuation?.resume(throwing: IdCardInternalError.cancelledByUser) + return + + default: + break + } + } + continuation?.resume(throwing: error) + } +} diff --git a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift index bbfe96a4..5d3c8d88 100644 --- a/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift +++ b/RIADigiDoc/Domain/NFC/OperationReadCertAndSign.swift @@ -30,19 +30,12 @@ import LibdigidocLibSwift import UtilsLib @MainActor -public class OperationReadCertAndSign: NSObject, Loggable { - private var session: NFCTagReaderSession? - private var isFinished = false - private var canNumber: String = "" +public class OperationReadCertAndSign: NFCOperationBase { private var pin2Number: SecureData = SecureData([0x00]) private var signedContainer: SignedContainerProtocol? private var containerPath: URL? private var roleData: RoleData? private var userAgent: String = "" - private var nfcError: String? = "" - private var strings: NFCSessionStrings? - - private let connection = NFCConnection() private var continuation: CheckedContinuation? @@ -79,50 +72,9 @@ public class OperationReadCertAndSign: NSObject, Loggable { } } - private func updateAlertMessage(step: Int) { - let stepMessages = [ - strings?.initialMessage ?? "", - strings?.step1Message ?? "", - strings?.step2Message ?? "", - strings?.step3Message ?? "", - strings?.step4Message ?? "" - ] - - let stepMessage = stepMessages[min(step, stepMessages.count - 1)] - let progressBar = ProgressBar(currentStep: step) - var message = stepMessage - OperationReadCertAndSign.logger().info("NFC: Updating alert message to: \(message)") - message += "\n\n\(progressBar.generate())" - session?.alertMessage = message - } - - private func success() { - session?.alertMessage = strings?.successMessage ?? "" - session?.invalidate() - } - - private func handleIdCardError(_ error: IdCardError) { - switch error { - case .wrongCAN: - nfcError = strings?.canErrorMessage ?? "" - case .wrongPIN(let triesLeft): - if triesLeft > 1 { - nfcError = strings?.pinWrongMultipleErrorMessage ?? "" - } else if triesLeft == 1 { - nfcError = strings?.pinWrongErrorMessage ?? "" - } else { - nfcError = strings?.pinBlockedErrorMessage ?? "" - } - case .sessionError: - nfcError = strings?.sessionErrorMessage ?? "" - default: - nfcError = strings?.technicalErrorMessage ?? "" - } - } -} + // MARK: - NFCTagReaderSessionDelegate -extension OperationReadCertAndSign: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { Task { @MainActor in defer { self.session = nil @@ -131,28 +83,32 @@ extension OperationReadCertAndSign: @MainActor NFCTagReaderSessionDelegate { guard let signedContainer else { let error = ReadCertAndSignError.signedContainerNil OperationReadCertAndSign.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "Failed to read container data") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to read container data") continuation?.resume(throwing: error) return } guard let roleData else { let error = ReadCertAndSignError.roleDataNil OperationReadCertAndSign.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "Failed to read role data") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to read role data") continuation?.resume(throwing: error) return } guard let containerPath else { let error = ReadCertAndSignError.containerPathNil OperationReadCertAndSign.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "Failed to read container path") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to read container path") continuation?.resume(throwing: error) return } if userAgent.isEmpty { let error = ReadCertAndSignError.userAgentEmpty OperationReadCertAndSign.logger().error("NFC: \(error.localizedDescription)") - session.invalidate(errorMessage: "Failed to initialize user agent") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Failed to initialize user agent") continuation?.resume(throwing: error) return } @@ -186,18 +142,10 @@ extension OperationReadCertAndSign: @MainActor NFCTagReaderSessionDelegate { continuation?.resume(with: .success(result)) success() } catch { - guard !isFinished else { - OperationReadCertAndSign.logger() - .info("NFC: Operation already finished, ignoring error: \(error.localizedDescription)") - return - } - isFinished = true + guard !checkIfFinished(error: error) else { return } if let idCardInternalError = error as? IdCardInternalError { - let idCardError = idCardInternalError.getIdCardError() - OperationReadCertAndSign.logger().error("NFC: IdCardError detected: \(idCardError)") - handleIdCardError(idCardError) - session.invalidate(errorMessage: nfcError ?? "") + handleIdCardInternalError(idCardInternalError, session: session) continuation?.resume(throwing: error) return } @@ -217,18 +165,13 @@ extension OperationReadCertAndSign: @MainActor NFCTagReaderSessionDelegate { return } - OperationReadCertAndSign.logger().error("NFC: Unknown error type: \(type(of: error))") - OperationReadCertAndSign.logger().error("NFC: Error details: \(error.localizedDescription)") - let wrappedError = ReadCertAndSignError.unknown(error) - session.invalidate(errorMessage: strings?.sessionErrorMessage ?? "") + let wrappedError = ReadCertAndSignError.unknown(handleUnknownError(error, session: session)) continuation?.resume(throwing: wrappedError) } } } - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { + public override func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { self.session = nil guard !isFinished else { return } isFinished = true @@ -244,5 +187,4 @@ extension OperationReadCertAndSign: @MainActor NFCTagReaderSessionDelegate { } continuation?.resume(throwing: error) } - } diff --git a/RIADigiDoc/Domain/NFC/OperationUnblockPin.swift b/RIADigiDoc/Domain/NFC/OperationUnblockPin.swift new file mode 100644 index 00000000..1b58c12e --- /dev/null +++ b/RIADigiDoc/Domain/NFC/OperationUnblockPin.swift @@ -0,0 +1,131 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CoreNFC +import CommonCrypto +import CryptoTokenKit +internal import SwiftECC +import BigInt +import Security +import IdCardLib + +@MainActor +public class OperationUnblockPin: NFCOperationBase { + private var codeType: CodeType? + private var puk: SecureData? + private var newPin: SecureData? + private var continuation: CheckedContinuation? + + public func startReading( + canNumber: String, + codeType: CodeType, + puk: SecureData, + newPin: SecureData, + strings: NFCSessionStrings + ) async throws { + return try await withCheckedThrowingContinuation { continuation in + self.continuation = continuation + + guard NFCTagReaderSession.readingAvailable else { + continuation.resume(throwing: IdCardInternalError.nfcNotSupported) + return + } + + self.canNumber = canNumber + self.codeType = codeType + self.puk = puk + self.newPin = newPin + self.strings = strings + session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self) + updateAlertMessage(step: 0) + session?.begin() + } + } + + // MARK: - NFCTagReaderSessionDelegate + + public override func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + Task { @MainActor in + defer { + self.session = nil + } + + guard let codeType = self.codeType, + let puk = self.puk, + let newPin = self.newPin else { + let error = UnblockPINError.missingRequiredParameter + OperationUnblockPin.logger().error("NFC: \(error.localizedDescription)") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? + "Missing required parameters") + continuation?.resume(throwing: error) + return + } + do { + updateAlertMessage(step: 1) + let tag = try await self.connection.setup(session, tags: tags) + + updateAlertMessage(step: 2) + let cardCommands = try await self.connection.getCardCommands(session, tag: tag, CAN: self.canNumber) + + updateAlertMessage(step: 3) + try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin) + + self.continuation?.resume(with: .success(())) + success() + } catch { + guard !checkIfFinished(error: error) else { return } + + if let idCardInternalError = error as? IdCardInternalError { + handleIdCardInternalError(idCardInternalError, session: session) + continuation?.resume(throwing: error) + return + } + + if let unblockPINError = error as? UnblockPINError { + OperationReadCertAndSign.logger() + .error("NFC: UnblockPINError: \(unblockPINError.localizedDescription)") + session.invalidate(errorMessage: strings?.technicalErrorMessage ?? "") + continuation?.resume(throwing: error) + return + } + + let wrappedError = UnblockPINError.unknown(handleUnknownError(error, session: session)) + continuation?.resume(throwing: wrappedError) + } + } + } + + public override func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Error) { + self.session = nil + guard !isFinished else { return } + isFinished = true + if let nfcError = error as? NFCReaderError { + switch nfcError.code { + case .readerSessionInvalidationErrorUserCanceled: + continuation?.resume(throwing: IdCardInternalError.cancelledByUser) + return + + default: + break + } + } + continuation?.resume(throwing: error) + } +} diff --git a/RIADigiDoc/Domain/Preferences/DataStore.swift b/RIADigiDoc/Domain/Preferences/DataStore.swift index 9fe35f53..1ee5328d 100644 --- a/RIADigiDoc/Domain/Preferences/DataStore.swift +++ b/RIADigiDoc/Domain/Preferences/DataStore.swift @@ -459,21 +459,12 @@ public actor DataStore: DataStoreProtocol { userDefaults().set(roleData.zipCode, forKey: Keys.roleZipCode) } - public func getNFCInputData() async -> NFCInputData { - let canNumber = userDefaults().string( - forKey: Keys.nfcCanNumber - ) ?? DefaultValues.nfcCanNumber - let rememberMe = userDefaults().object(forKey: Keys.nfcRememberMe) as? Bool ?? true - - return NFCInputData( - canNumber: canNumber, - rememberMe: rememberMe - ) + public func getNFCRememberMe() async -> Bool { + return userDefaults().object(forKey: Keys.nfcRememberMe) as? Bool ?? true } - public func setNFCInputData(_ inputData: NFCInputData) async { - userDefaults().set(inputData.canNumber, forKey: Keys.nfcCanNumber) - userDefaults().set(inputData.rememberMe, forKey: Keys.nfcRememberMe) + public func setNFCRememberMe(_ value: Bool) async { + userDefaults().set(value, forKey: Keys.nfcRememberMe) } // MARK: - Logging @@ -532,7 +523,6 @@ public actor DataStore: DataStoreProtocol { static let roleState = "" static let roleCountry = "" static let roleZipCode = "" - static let nfcCanNumber = "" } private enum Keys { @@ -573,7 +563,6 @@ public actor DataStore: DataStoreProtocol { static let roleState = "roleState" static let roleCountry = "roleCountry" static let roleZipCode = "roleZipCode" - static let nfcCanNumber = "nfcCanNumber" static let nfcRememberMe = "nfcRememberMe" static let enableLoggingNextSession = "enableLoggingNextSession" static let enableLoggingThisSession = "enableLoggingThisSession" diff --git a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift index 9e6ef159..9a0e7525 100644 --- a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift +++ b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift @@ -108,8 +108,8 @@ public protocol DataStoreProtocol: Sendable { func setRoleData(_ roleData: RoleData) async // MARK: - NFC Input Methods - func getNFCInputData() async -> NFCInputData - func setNFCInputData(_ inputData: NFCInputData) async + func getNFCRememberMe() async -> Bool + func setNFCRememberMe(_ value: Bool) async // MARK: - Logging func getEnableLoggingNextSession() async -> Bool diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 4940ad6d..e7d8f084 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -256,7 +256,10 @@ struct IdCardView: View { await MainActor.run { isInProgress = true pathManager.replaceLast( - to: .myEidView(idCardData: cardData) + to: .myEidView( + idCardData: cardData, + actionMethod: .idCardViaUSB + ) ) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift index 57f91889..aab4590f 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift @@ -31,13 +31,14 @@ struct NFCInputView: View { @Binding var rememberMe: Bool @Binding var isActionEnabled: Bool @Binding var canNumberError: String? - @Binding var pinNumber: String @Binding var pinError: String? - var pinType: CodeType? + var pinType: CodeType? let onInputChange: () -> Void + @State private var showPinField: Bool + private var canNumberTitle: String { languageSettings.localized("CAN number") } @@ -62,7 +63,8 @@ struct NFCInputView: View { pinNumber: Binding, pinError: Binding, pinType: CodeType?, - onInputChange: @escaping () -> Void + onInputChange: @escaping () -> Void, + showPinField: Bool = true, ) { self._canNumber = canNumber self._rememberMe = rememberMe @@ -72,6 +74,7 @@ struct NFCInputView: View { self._pinError = pinError self.pinType = pinType self.onInputChange = onInputChange + self.showPinField = showPinField } var body: some View { @@ -98,18 +101,20 @@ struct NFCInputView: View { } .padding(.vertical, Dimensions.Padding.MPadding) - VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { - FloatingLabelTextField( - title: pinNumberTitle, - placeholder: pinNumberTitle, - text: $pinNumber, - isSecure: true, - isError: !(pinError?.isEmpty ?? true), - errorText: pinError ?? "", - keyboardType: .numberPad - ) - .onChange(of: pinNumber) { - onInputChange() + if showPinField { + VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { + FloatingLabelTextField( + title: pinNumberTitle, + placeholder: pinNumberTitle, + text: $pinNumber, + isSecure: true, + isError: !(pinError?.isEmpty ?? true), + errorText: pinError ?? "", + keyboardType: .numberPad + ) + .onChange(of: pinNumber) { + onInputChange() + } } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index e8871b0c..f66eab9b 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -46,6 +46,7 @@ struct NFCView: View { @State private var taskSign: Task? @State private var taskDecrypt: Task? + @State private var taskMyeid: Task? private var isNFCSupported: Bool { viewModel.isNFCSupported() @@ -89,6 +90,12 @@ struct NFCView: View { ) } + private var nfcStringsUtil: NFCSessionStringsUtil { + NFCSessionStringsUtil { key, args in + languageSettings.localized(key, args) + } + } + let signedContainer: SignedContainerProtocol? let cryptoContainer: CryptoContainerProtocol? @@ -124,6 +131,7 @@ struct NFCView: View { onBackClick: { cancelDecrypt() cancelSigning() + cancelMyeid() guard isInProgress else { dismiss() return @@ -153,28 +161,9 @@ struct NFCView: View { } case .myeid: saveInputData() - // TODO: Implement My eID personal data loading action isInProgress = true - // TODO: Replace with real loading - Task { - try? await Task.sleep(for: .seconds(1)) - pathManager.replaceLast( - to: .myEidView( - idCardData: IdCardData( - publicData: CardInfo(), - authCertNotValidDate: nil, - signCertNotValidDate: nil, - retryCount: RetryCount( - pin1: 3, - pin2: 3, - puk: 3 - ), - isPUKChangeable: true - ) - ) - ) - } + loadMyEid() } }, content: { @@ -195,8 +184,14 @@ struct NFCView: View { pinType: pinType, onInputChange: { isActionEnabled = viewModel - .isActionEnabled(canNumber: canNumber, pinNumber: pinNumber, pinType: pinType) - } + .isActionEnabled( + canNumber: canNumber, + pinNumber: pinNumber, + pinType: pinType, + actionType: actionType + ) + }, + showPinField: actionType != .myeid ) } } @@ -281,28 +276,7 @@ struct NFCView: View { isInProgress = true nfcActionMessage = "NFC hold card" - let pinName = CodeType.pin1.name - let strings = NFCSessionStrings( - initialMessage: languageSettings.localized("Please place your ID card against the smart device"), - step1Message: - languageSettings.localized( - "Hold your ID card against your smart device until the data is read" - ), - step2Message: languageSettings.localized("Reading data"), - step3Message: languageSettings.localized("Reading certificate"), - step4Message: languageSettings.localized("Decrypting in progress"), - successMessage: languageSettings.localized("Data read"), - canErrorMessage: languageSettings.localized("Wrong CAN"), - pinWrongMultipleErrorMessage: - languageSettings.localized( - "PIN verification error multiple", - [pinName, "2"] - ), - pinWrongErrorMessage: languageSettings.localized("PIN verification error one", [pinName]), - pinBlockedErrorMessage: languageSettings.localized("PIN blocked", [pinName]), - technicalErrorMessage: languageSettings.localized("NFC technical error"), - sessionErrorMessage: languageSettings.localized("NFC session error") - ) + let strings = nfcStringsUtil.makeForDecrypt(pinName: CodeType.pin1.name) let decryptedContainer = await viewModel.decrypt( CAN: canNumber, @@ -341,6 +315,11 @@ struct NFCView: View { taskSign = nil } + private func cancelMyeid() { + taskMyeid?.cancel() + taskMyeid = nil + } + private func sign(roleData: RoleData? = nil) { taskSign = Task { guard let container = signedContainer else { return } @@ -353,28 +332,7 @@ struct NFCView: View { isInProgress = true nfcActionMessage = "NFC hold card" - let pinName = CodeType.pin2.name - let strings = NFCSessionStrings( - initialMessage: languageSettings.localized("Please place your ID card against the smart device"), - step1Message: - languageSettings.localized( - "Hold your ID card against your smart device until the data is read" - ), - step2Message: languageSettings.localized("Reading data please wait"), - step3Message: languageSettings.localized("Reading certificate"), - step4Message: languageSettings.localized("Signing in progress please wait"), - successMessage: languageSettings.localized("Signature added"), - canErrorMessage: languageSettings.localized("Wrong CAN"), - pinWrongMultipleErrorMessage: - languageSettings.localized( - "PIN verification error multiple", - [pinName, "2"] - ), - pinWrongErrorMessage: languageSettings.localized("PIN verification error one", [pinName]), - pinBlockedErrorMessage: languageSettings.localized("PIN blocked", [pinName]), - technicalErrorMessage: languageSettings.localized("NFC technical error"), - sessionErrorMessage: languageSettings.localized("NFC session error") - ) + let strings = nfcStringsUtil.makeForSigning(pinName: CodeType.pin2.name) let updatedContainer = await viewModel.sign( canNumber: canNumber, @@ -400,6 +358,40 @@ struct NFCView: View { dismiss() } } + + private func loadMyEid() { + taskMyeid = Task { + await viewModel.saveInputData( + canNumber: rememberMe ? canNumber : "", + rememberMe: rememberMe + ) + + isInProgress = true + nfcActionMessage = "NFC hold card" + + let strings = nfcStringsUtil.makeDefault() + let cardData = await viewModel.readCardData( + CAN: canNumber, + strings: strings + ) + + isInProgress = false + guard let cardData else { + cancelMyeid() + return + } + + viewModel.saveMyEidCAN(canNumber) + await MainActor.run { + pathManager.replaceLast( + to: .myEidView( + idCardData: cardData, + actionMethod: .idCardViaNFC + ) + ) + } + } + } } #Preview { diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift index 9a886d72..0575c2ef 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift @@ -154,14 +154,21 @@ struct MyEidPinChangeView: View { !viewModel.isPINLengthValid(for: currentCodeType, pin: pin) } + private var nfcStringsUtil: NFCSessionStringsUtil { + NFCSessionStringsUtil { key, args in + languageSettings.localized(key, args) + } + } + init( pinAction: MyEidPinCodeAction, codeType: CodeType, - personalCode: String + personalCode: String, + actionMethod: ActionMethod ) { self._viewModel = State( wrappedValue: Container.shared.myEidPinChangeViewModel( - (pinAction, codeType, personalCode) + (pinAction, codeType, personalCode, actionMethod) ) ) self.pinAction = pinAction @@ -223,7 +230,7 @@ struct MyEidPinChangeView: View { viewModel.input.isEmpty || !inputErrorMessage.isEmpty ) { return } - Task { await viewModel.submit() } + Task { await viewModel.submit(nfcStringsUtil: nfcStringsUtil) } } ) @@ -238,7 +245,7 @@ struct MyEidPinChangeView: View { text: buttonTitle, isButtonEnabled: !viewModel.input.isEmpty && !isInputError, action: { - Task { await viewModel.submit() } + Task { await viewModel.submit(nfcStringsUtil: nfcStringsUtil) } } ) } diff --git a/RIADigiDoc/UI/Component/My eID/MyEidView.swift b/RIADigiDoc/UI/Component/My eID/MyEidView.swift index a0b5f1ba..fe1709be 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidView.swift @@ -44,6 +44,7 @@ struct MyEidView: View { @State private var viewModel: MyEidViewModel private let idCardData: IdCardData + private let actionMethod: ActionMethod private var myDataTitle: String { languageSettings.localized("My data") @@ -77,10 +78,12 @@ struct MyEidView: View { } init( - idCardData: IdCardData + idCardData: IdCardData, + actionMethod: ActionMethod ) { _viewModel = State(wrappedValue: Container.shared.myEidViewModel()) self.idCardData = idCardData + self.actionMethod = actionMethod viewModel.setIsPinBlocked(.pin1, isBlocked: idCardData.retryCount.pin1 == 0) viewModel.setIsPinBlocked(.pin2, isBlocked: idCardData.retryCount.pin2 == 0) @@ -149,7 +152,8 @@ struct MyEidView: View { to: .myEidPinView( pinAction: .change, codeType: .pin1, - personalCode: idCardData.publicData.personalCode + personalCode: idCardData.publicData.personalCode, + actionMethod: actionMethod ) ) @@ -159,7 +163,8 @@ struct MyEidView: View { to: .myEidPinView( pinAction: .unblock, codeType: .pin1, - personalCode: idCardData.publicData.personalCode + personalCode: idCardData.publicData.personalCode, + actionMethod: actionMethod ) ) @@ -168,7 +173,8 @@ struct MyEidView: View { to: .myEidPinView( pinAction: .change, codeType: .pin2, - personalCode: idCardData.publicData.personalCode + personalCode: idCardData.publicData.personalCode, + actionMethod: actionMethod ) ) @@ -178,7 +184,8 @@ struct MyEidView: View { to: .myEidPinView( pinAction: .unblock, codeType: .pin2, - personalCode: idCardData.publicData.personalCode + personalCode: idCardData.publicData.personalCode, + actionMethod: actionMethod ) ) @@ -187,7 +194,8 @@ struct MyEidView: View { to: .myEidPinView( pinAction: .change, codeType: .puk, - personalCode: idCardData.publicData.personalCode + personalCode: idCardData.publicData.personalCode, + actionMethod: actionMethod ) ) } @@ -201,7 +209,8 @@ struct MyEidView: View { } } .task(id: viewModel.usbReaderStatus) { - if viewModel.usbReaderStatus != .sCardConnected { + if actionMethod == .idCardViaUSB && + viewModel.usbReaderStatus != .sCardConnected { await viewModel.stopDiscoveringReaders() await MainActor.run { pathManager.replaceLast(to: .myEidRootView) @@ -297,6 +306,7 @@ struct MyEidView: View { puk: 3 ), isPUKChangeable: true - ) + ), + actionMethod: .idCardViaNFC ) } diff --git a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift index 745ae79b..3fe07dcf 100644 --- a/RIADigiDoc/UI/Navigation/NavigationDestinations.swift +++ b/RIADigiDoc/UI/Navigation/NavigationDestinations.swift @@ -110,10 +110,15 @@ struct NavigationDestinations: ViewModifier { case .myEidRootView: MyEidRootView() - case .myEidView(let idCardData): - MyEidView(idCardData: idCardData) - case .myEidPinView(let pinAction, let codeType, let personalCode): - MyEidPinChangeView(pinAction: pinAction, codeType: codeType, personalCode: personalCode) + case .myEidView(let idCardData, let actionMethod): + MyEidView(idCardData: idCardData, actionMethod: actionMethod) + case .myEidPinView(let pinAction, let codeType, let personalCode, let actionMethod): + MyEidPinChangeView( + pinAction: pinAction, + codeType: codeType, + personalCode: personalCode, + actionMethod: actionMethod + ) } } } diff --git a/RIADigiDoc/Util/EncryptedData/EncryptedDataUtil.swift b/RIADigiDoc/Util/EncryptedData/EncryptedDataUtil.swift new file mode 100644 index 00000000..34354df3 --- /dev/null +++ b/RIADigiDoc/Util/EncryptedData/EncryptedDataUtil.swift @@ -0,0 +1,99 @@ +/* + * Copyright 2017 - 2025 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import Foundation +import CryptoKit +import UtilsLib + +public actor EncryptedDataUtil: Loggable { + + // MARK: - Private Properties + + private static func applicationSupportDirectory() -> URL? { + return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first + } + + // MARK: - Key Management + + private static func storeKey(_ key: SymmetricKey, to url: URL) throws { + let keyData = key.withUnsafeBytes { Data($0) } + try keyData.write(to: url, options: .atomic) + } + + @discardableResult + public static func saveSymmetricKeyToAppSupport(fileName: String) throws -> URL { + guard let appSupportDirectory = applicationSupportDirectory() else { + EncryptedDataUtil.logger().error("Unable to locate Application Support directory") + throw EncryptedDataError.unableToLocateAppSupportDirectory + } + + let symmetricKeyURL = appSupportDirectory.appendingPathComponent(fileName) + let symmetricKey = SymmetricKey(size: .bits256) + + try storeKey(symmetricKey, to: symmetricKeyURL) + + EncryptedDataUtil.logger().info("Symmetric key saved to: \(symmetricKeyURL.path)") + return symmetricKeyURL + } + + public static func getSymmetricKey(fileName: String) throws -> SymmetricKey { + guard let appSupportDirectory = applicationSupportDirectory() else { + EncryptedDataUtil.logger().error("Unable to locate Application Support directory") + throw EncryptedDataError.unableToLocateAppSupportDirectory + } + + let symmetricKeyURL = appSupportDirectory.appendingPathComponent(fileName) + + guard FileManager.default.fileExists(atPath: symmetricKeyURL.path) else { + EncryptedDataUtil.logger().error("Key file does not exist at: \(symmetricKeyURL.path)") + throw EncryptedDataError.keyFileDoesNotExist + } + + let keyData = try Data(contentsOf: symmetricKeyURL) + return SymmetricKey(data: keyData) + } + + // MARK: - Encryption/Decryption + + public static func encryptSecret(_ secret: String, with key: SymmetricKey) -> Data? { + guard let secretData = secret.data(using: .utf8) else { + EncryptedDataUtil.logger().error("Unable to convert secret to data") + return nil + } + + do { + let sealedBox = try ChaChaPoly.seal(secretData, using: key) + return sealedBox.combined + } catch { + EncryptedDataUtil.logger().error("Unable to encrypt secret: \(error.localizedDescription)") + return nil + } + } + + public static func decryptSecret(_ data: Data, with symmetricKey: SymmetricKey) -> String? { + do { + let sealedBox = try ChaChaPoly.SealedBox(combined: data) + let decryptedData = try ChaChaPoly.open(sealedBox, using: symmetricKey) + return String(data: decryptedData, encoding: .utf8) + } catch { + EncryptedDataUtil.logger().error("Unable to decrypt secret: \(error.localizedDescription)") + return nil + } + } +} diff --git a/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift b/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift index ea526017..77d234a7 100644 --- a/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift +++ b/RIADigiDoc/ViewModel/MyEid/MyEidPinChangeViewModel.swift @@ -28,6 +28,7 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { private(set) var pinAction: MyEidPinCodeAction private(set) var codeType: CodeType private(set) var personalCode: String + private(set) var actionMethod: ActionMethod private(set) var steps: [MyEidPinCodeStep] = [] private(set) var stepIndex: Int = 0 @@ -59,12 +60,14 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { pinAction: MyEidPinCodeAction, codeType: CodeType, personalCode: String, + actionMethod: ActionMethod, idCardRepository: IdCardRepositoryProtocol, - sharedMyEidSession: SharedMyEidSessionProtocol + sharedMyEidSession: SharedMyEidSessionProtocol, ) { self.pinAction = pinAction self.codeType = codeType self.personalCode = personalCode + self.actionMethod = actionMethod self.idCardRepository = idCardRepository self.sharedMyEidSession = sharedMyEidSession @@ -81,7 +84,9 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { } } - func submit() async { + func submit( + nfcStringsUtil: NFCSessionStringsUtil + ) async { resetInputError() resetErrorMessage() @@ -102,7 +107,6 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { clearPinCodes() return } - guard let currentPinCode = currentCode, let newPinCode = newCode else { input = "" clearPinCodes() @@ -115,7 +119,8 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { pinAction, codeType: codeType, current: currentPinCode, - new: newPinCode + new: newPinCode, + nfcStringsUtil: nfcStringsUtil ) } @@ -228,28 +233,56 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { _ action: MyEidPinCodeAction, codeType: CodeType, current: [UInt8], - new: [UInt8] + new: [UInt8], + nfcStringsUtil: NFCSessionStringsUtil ) async { do { - switch action { - case .change: - try await idCardRepository.changeCode( - codeType, - to: Data(new), - verifyCode: Data(current) - ) - case .unblock: - try await idCardRepository - .unblockCode(codeType, puk: Data(current), newCode: Data(new)) - sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: false) + if actionMethod == .idCardViaUSB { + switch action { + case .change: + try await idCardRepository.changeCode( + codeType, + to: Data(new), + verifyCode: Data(current) + ) + case .unblock: + try await idCardRepository + .unblockCode(codeType, puk: Data(current), newCode: Data(new)) + sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: false) + } + } else if actionMethod == .idCardViaNFC { + let canNumber = sharedMyEidSession.getCAN() + switch action { + case .change: + try await OperationChangePin().startChanging( + canNumber: canNumber, + codeType: codeType, + currentPin: SecureData(current), + newPin: SecureData(new), + strings: nfcStringsUtil.makeForChangePin(pinName: codeType.name) + ) + case .unblock: + try await OperationUnblockPin().startReading( + canNumber: canNumber, + codeType: codeType, + puk: SecureData(current), + newPin: SecureData(new), + strings: nfcStringsUtil.makeForUnblock(pinName: codeType.name) + ) + sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: false) + } } isSuccess = true } catch { - MyEidPinChangeViewModel.logger().error("Unable to change or unblock PIN. \(error)") + MyEidPinChangeViewModel.logger().error("Unable to change or unblock PIN.") - if let pinCodeChangeError = error as? IdCardError { - handleError(pinCodeChangeError) + if let idCardInternalError = error as? IdCardInternalError { + let idCardError = idCardInternalError.getIdCardError() + MyEidPinChangeViewModel.logger().error("NFC: IdCardError: \(idCardError)") + handleIdCardError(idCardError, pinType: codeType) } else { + MyEidPinChangeViewModel.logger().error("NFC: Unexpected error type: \(type(of: error))") + MyEidPinChangeViewModel.logger().error("NFC: Error details: \(error)") errorMessage = "General error" } @@ -259,19 +292,34 @@ final class MyEidPinChangeViewModel: MyEidPinChangeViewModelProtocol, Loggable { clearPinCodes() } - private func handleError(_ error: IdCardError) { + private func handleIdCardError(_ error: IdCardError, pinType: CodeType) { + MyEidPinChangeViewModel.logger().error("ID Card error: \(error)") + switch error { - case .wrongPIN(triesLeft: 0): - errorMessage = "PIN blocked" - errorMessageExtraArguments = [codeType.name] - isBlocked = true - sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: true) - case .wrongPIN(let remaining): - errorMessage = remaining > 1 ? "PIN verification error multiple" : "PIN verification error one" - errorMessageExtraArguments = [ - pinAction == .change ? codeType.name : CodeType.puk.name, String(remaining) - ] - resetToCurrentPinEntryStep() + case .cancelledByUser: + errorMessage = nil + errorMessageExtraArguments = [] + case .wrongCAN: + errorMessage = "Wrong CAN" + errorMessageExtraArguments = [] + case .wrongPIN(let triesLeft): + if triesLeft > 1 { + errorMessage = "PIN verification error multiple" + errorMessageExtraArguments = [pinType.name, String(triesLeft)] + resetToCurrentPinEntryStep() + } else if triesLeft == 1 { + errorMessage = "PIN verification error one" + errorMessageExtraArguments = [pinType.name] + resetToCurrentPinEntryStep() + } else { + errorMessage = "PIN blocked" + errorMessageExtraArguments = [pinType.name] + isBlocked = true + sharedMyEidSession.setIsPinBlocked(codeType, isBlocked: true) + } + case .sessionError: + errorMessage = "NFC session error" + errorMessageExtraArguments = [] default: resetInputError() errorMessage = "General error" diff --git a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift index be41624a..0ce6d1ed 100644 --- a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift +++ b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSession.swift @@ -30,6 +30,8 @@ final class SharedMyEidSession: SharedMyEidSessionProtocol { private var isPin2Blocked = false private var isPukBlocked = false + private var canNumber = "" + private let idCardRepository: IdCardRepositoryProtocol private var task: Task? @@ -71,4 +73,14 @@ final class SharedMyEidSession: SharedMyEidSessionProtocol { public func stopStatusStream() { task?.cancel() } + + // MARK: - NFC methods + + public func setCAN(_ can: String) { + self.canNumber = can + } + + public func getCAN() -> String { + self.canNumber + } } diff --git a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift index cac7cec5..bb713de4 100644 --- a/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift +++ b/RIADigiDoc/ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift @@ -27,4 +27,6 @@ public protocol SharedMyEidSessionProtocol: Sendable { func setIsPinBlocked(_ codeType: CodeType, isBlocked: Bool) func getIsPinBlocked(for codeType: CodeType) -> Bool func stopStatusStream() + func setCAN(_ can: String) + func getCAN() -> String } diff --git a/RIADigiDoc/ViewModel/Protocols/MyEid/MyEidPinChangeViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/MyEid/MyEidPinChangeViewModelProtocol.swift index 84235597..f338ee45 100644 --- a/RIADigiDoc/ViewModel/Protocols/MyEid/MyEidPinChangeViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/MyEid/MyEidPinChangeViewModelProtocol.swift @@ -30,7 +30,7 @@ protocol MyEidPinChangeViewModelProtocol: Sendable { var errorMessageExtraArguments: [String] { get } var isBlocked: Bool { get } - func submit() async + func submit(nfcStringsUtil: NFCSessionStringsUtil) async func resetErrors() func verifyNewCode() diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index a394f08f..cb827138 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -44,15 +44,26 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { var nfcAlertMessageExtraArguments: [String] = [] var nfcAlertMessageUrl: String? + private let nfcCANKeyFilename = Constants.File.nfcCANKey + private let dataStore: DataStoreProtocol private let userAgentUtil: UserAgentUtilProtocol + private let certificateUtil: CertificateUtilProtocol + private let sharedMyEidSession: SharedMyEidSessionProtocol + private let keychainStore: KeychainStoreProtocol init( dataStore: DataStoreProtocol, - userAgentUtil: UserAgentUtilProtocol + userAgentUtil: UserAgentUtilProtocol, + certificateUtil: CertificateUtilProtocol, + sharedMyEidSession: SharedMyEidSessionProtocol, + keychainStore: KeychainStoreProtocol ) { self.dataStore = dataStore self.userAgentUtil = userAgentUtil + self.certificateUtil = certificateUtil + self.sharedMyEidSession = sharedMyEidSession + self.keychainStore = keychainStore } func isNFCSupported() -> Bool { @@ -61,26 +72,121 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { return false } - func isActionEnabled(canNumber: String, pinNumber: String, pinType: CodeType?) -> Bool { + func isActionEnabled( + canNumber: String, + pinNumber: String, + pinType: CodeType?, + actionType: ActionType? = nil + ) -> Bool { checkCANNumberValidity(canNumber: canNumber) + let canNumberValid = (!canNumber.isEmpty && canNumberErrorKey?.isEmpty == true) + if actionType == .myeid { + return canNumberValid + } + checkPINNumberValidity(pinNumber: pinNumber, pinType: pinType) - let result = (!canNumber.isEmpty && canNumberErrorKey?.isEmpty == true) + let result = canNumberValid && (!pinNumber.isEmpty && pinNumberErrorKey?.isEmpty == true) return result } func saveInputData(canNumber: String, rememberMe: Bool) async { - await dataStore - .setNFCInputData( - NFCInputData( - canNumber: canNumber, - rememberMe: rememberMe - ) - ) + await dataStore.setNFCRememberMe(rememberMe) + + if rememberMe && !canNumber.isEmpty { + await saveEncryptedCAN(canNumber) + } else { + await keychainStore.remove(key: .nfcCANKey) + } + } func getInputData() async -> NFCInputData { - return await dataStore.getNFCInputData() + let rememberMe = await dataStore.getNFCRememberMe() + + if rememberMe { + if let decryptedCAN = await retrieveEncryptedCAN() { + return NFCInputData( + canNumber: decryptedCAN, + rememberMe: true + ) + } + } + + return NFCInputData(canNumber: "", rememberMe: rememberMe) + } + + private func saveEncryptedCAN(_ can: String) async { + do { + let symmetricKey = try EncryptedDataUtil.getSymmetricKey(fileName: self.nfcCANKeyFilename) + + if let encryptedCAN = EncryptedDataUtil.encryptSecret(can, with: symmetricKey) { + let saved = await keychainStore.save( + key: .nfcCANKey, + info: encryptedCAN, + withPasscodeSetOnly: true + ) + + if saved { + NFCViewModel.logger().info("CAN encrypted and saved successfully") + } else { + NFCViewModel.logger().error("Failed to save encrypted CAN to keychain") + } + } else { + NFCViewModel.logger().error("Encryption failed for CAN string") + } + } catch { + do { + let symKeyURL = try EncryptedDataUtil.saveSymmetricKeyToAppSupport( + fileName: self.nfcCANKeyFilename + ) + let symKey = try EncryptedDataUtil.getSymmetricKey( + fileName: symKeyURL.lastPathComponent + ) + + if let encryptedCAN = EncryptedDataUtil.encryptSecret(can, with: symKey) { + let saved = await keychainStore.save( + key: .nfcCANKey, + info: encryptedCAN, + withPasscodeSetOnly: true + ) + + if saved { + NFCViewModel.logger().info("CAN encrypted and saved with new key") + } else { + NFCViewModel.logger().error("Failed to save encrypted CAN after creating new key") + } + } else { + NFCViewModel.logger().error("Encryption failed for CAN after saving new symmetric key") + } + } catch { + NFCViewModel.logger().error("Unable to save or retrieve symmetric key: \(error.localizedDescription)") + } + } + } + + private func retrieveEncryptedCAN() async -> String? { + do { + guard let encryptedCANData = await keychainStore.retrieve(key: .nfcCANKey) else { + NFCViewModel.logger().info("No encrypted CAN found in keychain") + return nil + } + + let symmetricKey = try EncryptedDataUtil.getSymmetricKey( + fileName: self.nfcCANKeyFilename + ) + + if let decryptedCAN = EncryptedDataUtil.decryptSecret(encryptedCANData, with: symmetricKey) { + NFCViewModel.logger().info("CAN decrypted successfully") + return decryptedCAN + } else { + NFCViewModel.logger().error("Failed to decrypt CAN") + return nil + } + } catch { + NFCViewModel.logger().error("Unable to get stored CAN symmetric key: \(error.localizedDescription)") + return nil + } } func resetErrors() { @@ -92,10 +198,6 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { nfcErrorExtraArguments = [] } - func loadPersonalData() { - // TODO: Implement with My eID - } - func decrypt( CAN: String, pin1: String, @@ -339,4 +441,80 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { } pinNumberErrorKey = "" } + + public func saveMyEidCAN(_ can: String) { + sharedMyEidSession.setCAN(can) + } + + public func readCardData( + CAN: String, + strings: NFCSessionStrings + ) async -> IdCardData? { + do { + let nfcCardData = try await OperationReadCardData().startReading( + canNumber: CAN, + strings: strings + ) + + let authCertNotValidDate = + try await getAuthenticationCertificateNotValidDate(nfcCardData.authenticationCertificate) + let signCertNotValidDate = + try await getSignatureCertificateNotValidDate(nfcCardData.signatureCertificate) + guard let authCertNotValidDate else { + NFCViewModel.logger().error("NFC: Failed to get authentication certificate not valid date") + nfcErrorKey = "General error" + return nil + } + guard let signCertNotValidDate else { + NFCViewModel.logger().error("NFC: Failed to get signing certificate not valid date") + nfcErrorKey = "General error" + return nil + } + + return IdCardData( + publicData: nfcCardData.publicData, + authCertNotValidDate: authCertNotValidDate, + signCertNotValidDate: signCertNotValidDate, + retryCount: nfcCardData.retryCount, + isPUKChangeable: nfcCardData.isPUKChangable + ) + } catch { + NFCViewModel.logger().error("NFC: Failed to read card data") + + if let idCardInternalError = error as? IdCardInternalError { + let idCardError = idCardInternalError.getIdCardError() + NFCViewModel.logger().error("NFC: IdCardError: \(idCardError)") + handleIdCardError(idCardError, pinType: .pin2) + return nil + } + + NFCViewModel.logger().error("NFC: Unexpected error type: \(type(of: error))") + NFCViewModel.logger().error("NFC: Error details: \(error)") + nfcErrorKey = "General error" + return nil + } + } + + private func getAuthenticationCertificateNotValidDate(_ authCertData: Data?) async throws -> String? { + guard let authCertData else { return nil } + let authCertificate = certificateUtil.certificate(from: authCertData) + guard let authCert = authCertificate else { return nil } + return try getNotValidDate(from: authCert) + } + + private func getSignatureCertificateNotValidDate(_ signCertData: Data?) async throws -> String? { + guard let signCertData else { return nil } + let signCertificate = certificateUtil.certificate(from: signCertData) + guard let signCert = signCertificate else { return nil } + return try getNotValidDate(from: signCert) + } + + private func getNotValidDate(from certificate: SecCertificate) throws -> String? { + let certificate = try Certificate(certificate) + let notValidAfter = certificate.notValidAfter + return DateUtil.getFormattedDateTime( + date: notValidAfter, + isUTC: false + ).date + } } diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModelProtocol.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModelProtocol.swift index 7b651980..20e4546d 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModelProtocol.swift @@ -29,7 +29,8 @@ public protocol NFCViewModelProtocol: Sendable { func isActionEnabled( canNumber: String, pinNumber: String, - pinType: CodeType? + pinType: CodeType?, + actionType: ActionType? ) -> Bool func saveInputData( @@ -56,7 +57,10 @@ public protocol NFCViewModelProtocol: Sendable { strings: NFCSessionStrings ) async -> SignedContainerProtocol? - func loadPersonalData() + func readCardData( + CAN: String, + strings: NFCSessionStrings + ) async -> IdCardData? func isRoleDataEnabled() async -> Bool }