From da1fdb435b0deb3b474fae65fd538eb24416c32c Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Sat, 17 Oct 2026 22:22:23 +0300 Subject: [PATCH] Simplify NFCConnection Signed-off-by: Raul Metsma --- .gitignore | 3 + .../nfclib/CardActions/CardReaderNFC.swift | 8 +- .../Extensions/NFCISO7816Tag+Extensions.swift | 19 +- Sources/nfclib/Operations/NFCConnection.swift | 136 ++++++++++---- .../Operations/OperationReadCertificate.swift | 91 ++-------- .../Operations/OperationReadPublicData.swift | 67 ++----- .../nfclib/Operations/OperationSignHash.swift | 86 ++------- .../Operations/OperationUnblockPin.swift | 82 ++------- .../OperationAuthenticateWithWebEID.swift | 170 ++++++------------ .../nfc-demo.xcodeproj/project.pbxproj | 17 +- mvoting-nfc/nfc-demo/Operator.swift | 51 +++--- .../Preview Assets.xcassets/Contents.json | 6 - 12 files changed, 252 insertions(+), 484 deletions(-) delete mode 100644 mvoting-nfc/nfc-demo/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/.gitignore b/.gitignore index e4c2d26..89e0687 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ timeline.xctimeline playground.xcworkspace .DS_Store + +## Swift Package Manager +.swiftpm/ diff --git a/Sources/nfclib/CardActions/CardReaderNFC.swift b/Sources/nfclib/CardActions/CardReaderNFC.swift index 65eb6ba..7ff2649 100644 --- a/Sources/nfclib/CardActions/CardReaderNFC.swift +++ b/Sources/nfclib/CardActions/CardReaderNFC.swift @@ -55,8 +55,12 @@ class CardReaderNFC: CardReader { var ksMac: Bytes = AES.Zero var SSC: Bytes = AES.Zero - init(_ tag: NFCISO7816Tag, CAN: String) async throws { - self.tag = SendableISO7816Tag(tag: tag) + init( + _ tag: NFCISO7816Tag, + CAN: String, + invalidationError: @escaping @Sendable @MainActor () -> Error? = { nil } + ) async throws { + self.tag = SendableISO7816Tag(tag: tag, invalidationError: invalidationError) CardReaderNFC.logger.debug("Select CardAccess") _ = try await self.tag.sendCommand(cls: 0x00, ins: 0xA4, p1Byte: 0x02, p2Byte: 0x0C, data: Data([0x01, 0x1C])) diff --git a/Sources/nfclib/Extensions/NFCISO7816Tag+Extensions.swift b/Sources/nfclib/Extensions/NFCISO7816Tag+Extensions.swift index fb0fd69..3a15bcc 100644 --- a/Sources/nfclib/Extensions/NFCISO7816Tag+Extensions.swift +++ b/Sources/nfclib/Extensions/NFCISO7816Tag+Extensions.swift @@ -8,6 +8,8 @@ import CryptoTokenKit @MainActor struct SendableISO7816Tag { var tag: NFCISO7816Tag + /// Why the reader session ended, if it has. Reported instead of the transceive error it causes. + var invalidationError: @Sendable @MainActor () -> Error? = { nil } } extension SendableISO7816Tag { @@ -28,14 +30,19 @@ extension SendableISO7816Tag { data: data, expectedResponseLength: leByte ) - let result: (Data, UInt8, UInt8) = try await withCheckedThrowingContinuation { continuation in - tag.sendCommand(apdu: apdu) { responseData, sw1, sw2, error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume(returning: (responseData, sw1, sw2)) + let result: (Data, UInt8, UInt8) + do { + result = try await withCheckedThrowingContinuation { continuation in + tag.sendCommand(apdu: apdu) { responseData, sw1, sw2, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: (responseData, sw1, sw2)) + } } } + } catch { + throw invalidationError() ?? error } switch result { case (_, 0x63, 0x00): diff --git a/Sources/nfclib/Operations/NFCConnection.swift b/Sources/nfclib/Operations/NFCConnection.swift index 203aa4e..f77ac49 100644 --- a/Sources/nfclib/Operations/NFCConnection.swift +++ b/Sources/nfclib/Operations/NFCConnection.swift @@ -4,51 +4,89 @@ import Foundation @preconcurrency import CoreNFC +// Allow CoreNFC types to cross async boundaries in this controlled context +extension NFCTagReaderSession: @unchecked @retroactive Sendable {} +extension NFCTag: @unchecked @retroactive Sendable {} + @MainActor -public class NFCConnection { - public init() {} - public func setup(_ session: NFCTagReaderSession, tags: [NFCTag]) async throws -> NFCISO7816Tag { - if tags.count > 1 { - session.invalidate(errorMessage: "Failed to read data") - throw IdCardInternalError.multipleTagsDetected - } +public class NFCConnection: NSObject, @MainActor NFCTagReaderSessionDelegate { + private var session: NFCTagReaderSession? + private var continuation: CheckedContinuation? + private var invalidationError: Error? - guard let firstTag = tags.first else { - session.invalidate(errorMessage: "Failed to read data") - throw IdCardInternalError.invalidTag - } + override private init() {} + /// Runs an operation on a new connection. On success the session is closed with `successMessage`, + /// on failure `onError` maps the error to the error that is thrown and the message the session is + /// invalidated with. Cancelling the calling task invalidates the session, and any failure after + /// cancellation is passed to `onError` as `cancelledByUser`. A failure after the session was + /// invalidated (e.g. the user cancelled the sheet or it timed out) is passed as that reason. + public static func run( + successMessage: String = "Data read", + onError: (Error) -> (error: Error, message: String) = { (error: $0, message: "Failed to read data") }, + _ operation: (NFCConnection) async throws -> T + ) async throws -> T { + let connection = NFCConnection() + defer { connection.session = nil } do { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - session.connect(to: firstTag) { error in - if let error { - continuation.resume(throwing: error) - } else { - continuation.resume() - } - } + let result = try await withTaskCancellationHandler { + try await operation(connection) + } onCancel: { + Task { @MainActor [weak connection] in connection?.session?.invalidate() } } + connection.session?.alertMessage = successMessage + connection.session?.invalidate() + return result } catch { - session.invalidate(errorMessage: "Failed to read data") - throw IdCardInternalError.connectionFailed + let error: Error = if Task.isCancelled || error is CancellationError { + IdCardInternalError.cancelledByUser + } else { + connection.invalidationError ?? error + } + let failure = onError(error) + connection.session?.invalidate(errorMessage: failure.message) + throw failure.error } + } - guard case let .iso7816(tag) = firstTag else { - session.invalidate(errorMessage: "Failed to read data") - throw IdCardInternalError.invalidTag + /// Starts the session showing `message`, and once a card is detected shows `detectedMessage` + /// while it is connected to and set up with `CAN`. + public func connect(CAN: String, message: String, detectedMessage: String) async throws -> CardCommands { + try Task.checkCancellation() + guard NFCTagReaderSession.readingAvailable else { + throw IdCardInternalError.nfcNotSupported + } + guard self.session == nil else { throw IdCardInternalError.operationInProgress } + guard let session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) else { + throw IdCardInternalError.nfcNotSupported + } + session.alertMessage = message + self.session = session + invalidationError = nil + return try await withTaskCancellationHandler { + let tag = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + self.continuation = continuation + session.begin() + } + session.alertMessage = detectedMessage + do { + try await session.connect(to: .iso7816(tag)) + } catch { + throw invalidationError ?? IdCardInternalError.connectionFailed + } + return try await cardCommands(tag: tag, CAN: CAN) + } onCancel: { + Task { @MainActor [weak self] in self?.session?.invalidate() } } - - return tag } - @MainActor - public func getCardCommands(_: NFCTagReaderSession, tag: NFCISO7816Tag, CAN: String) async throws -> CardCommands { - let initialSelectedAID = tag.initialSelectedAID - let reader = try await CardReaderNFC(tag, CAN: CAN) - guard let aid = Bytes(hex: initialSelectedAID) else { + private func cardCommands(tag: NFCISO7816Tag, CAN: String) async throws -> CardCommands { + guard let aid = Bytes(hex: tag.initialSelectedAID), !aid.isEmpty else { throw IdCardInternalError.connectionFailed } + let reader = try await CardReaderNFC(tag, CAN: CAN) { [weak self] in self?.invalidationError } + // Try Idemia with explicit AID if let cmd = Idemia(reader: reader, aid: aid) { return cmd @@ -68,5 +106,39 @@ public class NFCConnection { throw IdCardInternalError.cardNotSupported } -} + public func message(_ message: String) { + session?.alertMessage = message + } + + public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + guard tags.count == 1, let firstTag = tags.first else { + session.alertMessage = "More than one card detected, please present only your ID card" + Task { @MainActor [weak self] in + // Give the user time to remove the extra card before polling again + try? await Task.sleep(for: .milliseconds(500)) + self?.session?.restartPolling() + } + return + } + guard case let .iso7816(tag) = firstTag else { + session.alertMessage = "Card not supported, please present your ID card" + session.restartPolling() + return + } + continuation?.resume(returning: tag) + continuation = nil + } + + public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } + + public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { + guard session === self.session else { return } + self.session = nil + let error = IdCardInternalError.mapSessionInvalidation(error) + invalidationError = error + guard let continuation else { return } + self.continuation = nil + continuation.resume(throwing: error) + } +} diff --git a/Sources/nfclib/Operations/OperationReadCertificate.swift b/Sources/nfclib/Operations/OperationReadCertificate.swift index 0408579..07a151e 100644 --- a/Sources/nfclib/Operations/OperationReadCertificate.swift +++ b/Sources/nfclib/Operations/OperationReadCertificate.swift @@ -2,10 +2,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later import Foundation -import CoreNFC import Security - // MARK: - Local Types public enum ReadCertificateError: Error { @@ -19,89 +17,26 @@ public enum CertificateUsage { case sign } -// Allow CoreNFC types to cross async boundaries in this controlled context -extension NFCTagReaderSession: @unchecked @retroactive Sendable {} -extension NFCTag: @unchecked @retroactive Sendable {} - @MainActor -public class OperationReadCertificate: NSObject { - private var session: NFCTagReaderSession? - private var CAN: String = "" - private var certUsage: CertificateUsage! - private let nfcMessage: String = "Please place your ID card against the smart device" - private let connection = NFCConnection() - private var continuation: CheckedContinuation? +public class OperationReadCertificate { + public init() {} public func startReading(CAN: String, certUsage: CertificateUsage) async throws -> SecCertificate { - - return try await withCheckedThrowingContinuation { continuation in - guard self.continuation == nil else { - continuation.resume(throwing: IdCardInternalError.operationInProgress) - return - } - self.continuation = continuation - - guard NFCTagReaderSession.readingAvailable else { - self.finish(.failure(IdCardInternalError.nfcNotSupported)) - return - } - - self.CAN = CAN - self.certUsage = certUsage - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) - session?.alertMessage = nfcMessage - session?.begin() - } - } - - private func finish(_ result: Result) { - guard let continuation else { return } - self.continuation = nil - continuation.resume(with: result) - } -} - -extension OperationReadCertificate: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - Task { @MainActor in - guard let certUsage else { - finish(.failure(ReadCertificateError.certificateUsageNotSpecified)) - session.invalidate(errorMessage: "Failed to read data") - return - } + try await NFCConnection.run { connection in + let cardCommands = try await connection.connect( + CAN: CAN, + message: "Please place your ID card against the smart device", + detectedMessage: "Hold your ID card against your smart device until the data is read" + ) do { - session.alertMessage = "Hold your ID card against your smart device until the data is read" - let tag = try await connection.setup(session, tags: tags) - let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN) - do { - switch certUsage { - case .auth: - let cert = try await cardCommands.readAuthenticationCertificate() - let x509Certificate = try convertBytesToX509Certificate(cert) - finish(.success(x509Certificate)) - case .sign: - let cert = try await cardCommands.readSignatureCertificate() - let x509Certificate = try convertBytesToX509Certificate(cert) - finish(.success(x509Certificate)) - } - session.alertMessage = "Data read" - session.invalidate() - } catch { - finish(.failure(ReadCertificateError.failedToReadCertificate)) - session.invalidate(errorMessage: "Failed to read data") + let cert = switch certUsage { + case .auth: try await cardCommands.readAuthenticationCertificate() + case .sign: try await cardCommands.readSignatureCertificate() } + return try convertBytesToX509Certificate(cert) } catch { - finish(.failure(error)) - session.invalidate(errorMessage: "Failed to read data") + throw ReadCertificateError.failedToReadCertificate } } } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { - guard session === self.session else { return } - self.session = nil - finish(.failure(IdCardInternalError.mapSessionInvalidation(error))) - } } diff --git a/Sources/nfclib/Operations/OperationReadPublicData.swift b/Sources/nfclib/Operations/OperationReadPublicData.swift index b721737..5986039 100644 --- a/Sources/nfclib/Operations/OperationReadPublicData.swift +++ b/Sources/nfclib/Operations/OperationReadPublicData.swift @@ -2,66 +2,19 @@ // SPDX-License-Identifier: LGPL-2.1-or-later import Foundation -import CoreNFC -@MainActor final public class OperationReadPublicData: NSObject { - private var session: NFCTagReaderSession? - private var CAN: String = "" - private let nfcMessage: String = "Please place your ID card against the smart device" - private let connection = NFCConnection() - private var continuation: CheckedContinuation? +@MainActor +final public class OperationReadPublicData { + public init() {} public func startReading(CAN: String) async throws -> CardInfo { - return try await withCheckedThrowingContinuation { continuation in - guard self.continuation == nil else { - continuation.resume(throwing: IdCardInternalError.operationInProgress) - return - } - self.continuation = continuation - - guard NFCTagReaderSession.readingAvailable else { - self.finish(.failure(IdCardInternalError.nfcNotSupported)) - return - } - - self.CAN = CAN - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) - session?.alertMessage = nfcMessage - session?.begin() - } - } - - private func finish(_ result: Result) { - guard let continuation else { return } - self.continuation = nil - continuation.resume(with: result) - } -} - -extension OperationReadPublicData: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - Task { - do { - session.alertMessage = "Hold your ID card against your smart device until the data is read" - let tag = try await connection.setup(session, tags: tags) - let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN) - let cardInfo = try await cardCommands.readPublicData() - - finish(.success(cardInfo)) - session.alertMessage = "Data read" - session.invalidate() - } catch { - finish(.failure(error)) - session.invalidate(errorMessage: "Failed to read data") - } + try await NFCConnection.run { connection in + let cardCommands = try await connection.connect( + CAN: CAN, + message: "Please place your ID card against the smart device", + detectedMessage: "Hold your ID card against your smart device until the data is read" + ) + return try await cardCommands.readPublicData() } } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { - guard session === self.session else { return } - self.session = nil - finish(.failure(IdCardInternalError.mapSessionInvalidation(error))) - } } diff --git a/Sources/nfclib/Operations/OperationSignHash.swift b/Sources/nfclib/Operations/OperationSignHash.swift index 4931458..99f451a 100644 --- a/Sources/nfclib/Operations/OperationSignHash.swift +++ b/Sources/nfclib/Operations/OperationSignHash.swift @@ -2,42 +2,25 @@ // SPDX-License-Identifier: LGPL-2.1-or-later import Foundation -import CoreNFC @MainActor -public class OperationSignHash: NSObject { - private var session: NFCTagReaderSession? - private var CAN: String = "" - private var PIN: SecureData = SecureData([0x00]) - private var hashToSign: Data? - private let nfcMessage: String = "Please place your ID card against the smart device" - private var continuation: CheckedContinuation? - private var connection = NFCConnection() +public class OperationSignHash { + public init() {} public func startSigning(CAN: String, PIN2: SecureData, hash: Data) async throws -> Data { - - return try await withCheckedThrowingContinuation { continuation in - guard self.continuation == nil else { - continuation.resume(throwing: IdCardInternalError.operationInProgress) - return - } - self.continuation = continuation - - guard NFCTagReaderSession.readingAvailable else { - self.finish(.failure(IdCardInternalError.nfcNotSupported)) - return - } - self.CAN = CAN - self.PIN = PIN2 - self.hashToSign = hash - - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) - updateAlertMessage(step: 0) - session?.begin() + try await NFCConnection.run { connection in + let cardCommands = try await connection.connect( + CAN: CAN, + message: alertMessage(step: 0), + detectedMessage: alertMessage(step: 2) + ) + connection.message(alertMessage(step: 3)) + try Task.checkCancellation() + return try await cardCommands.calculateSignature(for: hash, withPin2: PIN2) } } - private func updateAlertMessage(step: Int) { + private func alertMessage(step: Int) -> String { let stepMessages = [ "Please place your ID card against the smart device", "Hold your ID card against your smart device until the data is read", @@ -47,49 +30,6 @@ public class OperationSignHash: NSObject { let stepMessage = stepMessages[min(step, stepMessages.count - 1)] let progressBar = ProgressBar(currentStep: step) - var message = stepMessage - message += "\n\n\(progressBar.generate())" - session?.alertMessage = message - } - - private func finish(_ result: Result) { - guard let continuation else { return } - self.continuation = nil - PIN = SecureData([0x00]) - continuation.resume(with: result) - } -} - -extension OperationSignHash: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - - Task { @MainActor in - do { - updateAlertMessage(step: 1) - guard let hashToSign else { - throw IdCardInternalError.invalidAPDU - } - let pin2 = PIN - let tag = try await connection.setup(session, tags: tags) - updateAlertMessage(step: 2) - let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN) - updateAlertMessage(step: 3) - let signatureValue = try await cardCommands.calculateSignature(for: hashToSign, withPin2: pin2) - finish(.success(signatureValue)) - session.alertMessage = "Data read" - session.invalidate() - } catch { - finish(.failure(error)) - session.invalidate(errorMessage: "Failed to read data") - } - } - } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { - guard session === self.session else { return } - self.session = nil - finish(.failure(IdCardInternalError.mapSessionInvalidation(error))) + return "\(stepMessage)\n\n\(progressBar.generate())" } } diff --git a/Sources/nfclib/Operations/OperationUnblockPin.swift b/Sources/nfclib/Operations/OperationUnblockPin.swift index 1caf169..4669125 100644 --- a/Sources/nfclib/Operations/OperationUnblockPin.swift +++ b/Sources/nfclib/Operations/OperationUnblockPin.swift @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later import Foundation -import CoreNFC public enum UnblockPINError: Error { case missingRequiredParameter @@ -10,79 +9,18 @@ public enum UnblockPINError: Error { } @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 class OperationUnblockPin { + public init() {} public func startReading(CAN: String, codeType: CodeType, puk: SecureData, newPin: SecureData) async throws { - - return try await withCheckedThrowingContinuation { continuation in - guard self.continuation == nil else { - continuation.resume(throwing: IdCardInternalError.operationInProgress) - return - } - self.continuation = continuation - - guard NFCTagReaderSession.readingAvailable else { - self.finish(.failure(IdCardInternalError.nfcNotSupported)) - return - } - - self.CAN = CAN - self.codeType = codeType - self.puk = puk - self.newPin = newPin - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) - session?.alertMessage = nfcMessage - session?.begin() + try await NFCConnection.run(successMessage: "PIN changed", onError: { (error: $0, message: "PIN change failed") }) { connection in + let cardCommands = try await connection.connect( + CAN: CAN, + message: "Please place your ID card against the smart device", + detectedMessage: "Hold your ID card against your smart device until the data is read" + ) + try Task.checkCancellation() + try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin) } } - - private func finish(_ result: Result) { - guard let continuation else { return } - self.continuation = nil - puk = nil - newPin = nil - continuation.resume(with: result) - } -} - -extension OperationUnblockPin: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - Task { @MainActor [weak self] in - guard let self else { return } - guard let codeType = self.codeType, let puk = self.puk, let newPin = self.newPin else { - finish(.failure(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) - try await cardCommands.unblockCode(codeType, puk: puk, newCode: newPin) - - finish(.success(())) - session.alertMessage = "PIN changed" - session.invalidate() - } catch { - finish(.failure(error)) - session.invalidate(errorMessage: "PIN change failed") - } - } - } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { - guard session === self.session else { return } - self.session = nil - finish(.failure(IdCardInternalError.mapSessionInvalidation(error))) - } } diff --git a/Sources/nfclib/WebEID/OperationAuthenticateWithWebEID.swift b/Sources/nfclib/WebEID/OperationAuthenticateWithWebEID.swift index 2dc1f79..c93f0ba 100644 --- a/Sources/nfclib/WebEID/OperationAuthenticateWithWebEID.swift +++ b/Sources/nfclib/WebEID/OperationAuthenticateWithWebEID.swift @@ -2,7 +2,6 @@ // SPDX-License-Identifier: LGPL-2.1-or-later import Foundation -import CoreNFC import Security public enum AuthenticateWithWebEidError: Error { @@ -14,41 +13,65 @@ public enum AuthenticateWithWebEidError: Error { } @MainActor -public class OperationAuthenticateWithWebEID: NSObject { - private let CAN: String - private var pin1: SecureData? - private let challenge: String - private let origin: String - private let connection = NFCConnection() +public class OperationAuthenticateWithWebEID { + public init() {} + + public func startReading(CAN: String, pin1: SecureData, challenge: String, origin: String) async throws -> WebEidData { + try await NFCConnection.run(onError: Self.failure) { connection in + let cardCommands = try await connection.connect( + CAN: CAN, + message: alertMessage(step: 0), + detectedMessage: alertMessage(step: 2) + ) + connection.message(alertMessage(step: 3)) + let certBytes = try await cardCommands.readAuthenticationCertificate() + let authCertificate = try convertBytesToX509Certificate(certBytes) + + let (notBefore, notAfter) = try validateCertificateStructure(authCertificate) + guard Date() >= notBefore else { + throw AuthenticateWithWebEidError.failedCertificateNotYetValid + } + guard Date() <= notAfter else { + throw AuthenticateWithWebEidError.failedCertificateExpired + } + guard let publicKey = SecCertificateCopyKey(authCertificate) else { + throw AuthenticateWithWebEidError.failedToReadPublicKey + } + + let algorithm = try WebEidSignatureAlgorithm.forAuthenticationKey(publicKey) + let hashLength = algorithm.hashLength - private var session: NFCTagReaderSession? - private var continuation: CheckedContinuation? + let originHash = sha(hashLength: hashLength, data: Bytes(origin.utf8)) + let challengeHash = sha(hashLength: hashLength, data: Bytes(challenge.utf8)) + let webEidHash = sha(hashLength: hashLength, data: originHash + challengeHash) - public init(CAN: String, pin1: SecureData, challenge: String, origin: String) { - self.CAN = CAN - self.pin1 = pin1 - self.challenge = challenge - self.origin = origin + connection.message(alertMessage(step: 4)) + try Task.checkCancellation() + let authResult = try await cardCommands.authenticate(for: webEidHash, withPin1: pin1) + let signingCertificateBytes = try await cardCommands.readSignatureCertificate() + + return WebEidData( + unverifiedCertificate: certBytes.base64EncodedString(), + algorithm: algorithm, + signature: authResult.base64EncodedString(), + signingCertificate: signingCertificateBytes.base64EncodedString() + ) + } } - public func startReading() async throws -> WebEidData { - return try await withCheckedThrowingContinuation { continuation in - guard self.continuation == nil else { - continuation.resume(throwing: IdCardInternalError.operationInProgress) - return - } - self.continuation = continuation - guard NFCTagReaderSession.readingAvailable else { - self.finish(.failure(IdCardInternalError.nfcNotSupported)) - return - } - session = NFCTagReaderSession(pollingOption: .iso14443, delegate: self, queue: DispatchQueue.main) - updateAlertMessage(step: 0) - session?.begin() + private static func failure(_ error: Error) -> (error: Error, message: String) { + let message = switch error { + case AuthenticateWithWebEidError.failedCertificateNotYetValid: + "Certificate not yet valid" + case AuthenticateWithWebEidError.failedCertificateExpired: + "Certificate has expired" + default: + "Failed to read data" } + return (error, message) } - private func updateAlertMessage(step: Int) { + private func alertMessage(step: Int) -> String { let stepMessages = [ "Please place your ID card against the smart device", "Hold your ID card against your smart device until the data is read", @@ -57,94 +80,7 @@ public class OperationAuthenticateWithWebEID: NSObject { ] let stepMessage = stepMessages[min(step, stepMessages.count - 1)] - let progressBar = ProgressBar(currentStep: step) - - var message = stepMessage - - message += "\n\n\(progressBar.generate())" - - session?.alertMessage = message - } - - private func finish(_ result: Result) { - guard let continuation else { return } - self.continuation = nil - pin1 = nil - continuation.resume(with: result) - } -} - -extension OperationAuthenticateWithWebEID: @MainActor NFCTagReaderSessionDelegate { - public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { - Task { @MainActor [weak self] in - guard let self else { return } - do { - updateAlertMessage(step: 1) - let tag = try await connection.setup(session, tags: tags) - updateAlertMessage(step: 2) - let cardCommands = try await connection.getCardCommands(session, tag: tag, CAN: CAN) - - updateAlertMessage(step: 3) - let certBytes = try await cardCommands.readAuthenticationCertificate() - let authCertificate = try convertBytesToX509Certificate(certBytes) - - let (notBefore, notAfter) = try validateCertificateStructure(authCertificate) - - guard Date() >= notBefore else { - let errorMessage = "Certificate not yet valid" - finish(.failure(AuthenticateWithWebEidError.failedCertificateNotYetValid)) - session.invalidate(errorMessage: errorMessage) - return - } - - guard Date() <= notAfter else { - let errorMessage = "Certificate has expired" - finish(.failure(AuthenticateWithWebEidError.failedCertificateExpired)) - session.invalidate(errorMessage: errorMessage) - return - } - - guard let publicKey = SecCertificateCopyKey(authCertificate) else { - let errorMessage = "Failed to read data" - finish(.failure(AuthenticateWithWebEidError.failedToReadPublicKey)) - session.invalidate(errorMessage: errorMessage) - return - } - - let algorithm = try WebEidSignatureAlgorithm.forAuthenticationKey(publicKey) - let hashLength = algorithm.hashLength - - let originHash = sha(hashLength: hashLength, data: Bytes(origin.utf8)) - let challengeHash = sha(hashLength: hashLength, data: Bytes(challenge.utf8)) - let webEidHash = sha(hashLength: hashLength, data: originHash + challengeHash) - - updateAlertMessage(step: 4) - guard let pin1 else { throw IdCardInternalError.pinVerificationFailed(codeType: .pin1) } - let authResult = try await cardCommands.authenticate(for: webEidHash, withPin1: pin1) - let signingCertificateBytes = try await cardCommands.readSignatureCertificate() - - let webEidData = WebEidData( - unverifiedCertificate: certBytes.base64EncodedString(), - algorithm: algorithm, - signature: authResult.base64EncodedString(), - signingCertificate: signingCertificateBytes.base64EncodedString() - ) - finish(.success(webEidData)) - session.alertMessage = "Data read" - session.invalidate() - } catch { - finish(.failure(error)) - session.invalidate(errorMessage: "Failed to read data") - } - } - } - - public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { } - - public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { - guard session === self.session else { return } - self.session = nil - finish(.failure(IdCardInternalError.mapSessionInvalidation(error))) + return "\(stepMessage)\n\n\(progressBar.generate())" } } diff --git a/mvoting-nfc/nfc-demo.xcodeproj/project.pbxproj b/mvoting-nfc/nfc-demo.xcodeproj/project.pbxproj index 711b41a..db12753 100644 --- a/mvoting-nfc/nfc-demo.xcodeproj/project.pbxproj +++ b/mvoting-nfc/nfc-demo.xcodeproj/project.pbxproj @@ -90,7 +90,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; - LastUpgradeCheck = 1500; + LastUpgradeCheck = 2700; TargetAttributes = { 139195A72AFA29CC00EDA022 = { CreatedOnToolsVersion = 15.0.1; @@ -146,7 +146,6 @@ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -174,10 +173,9 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5YYXE2JG54; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; @@ -197,6 +195,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -209,7 +208,6 @@ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; @@ -236,11 +234,9 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5YYXE2JG54; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -253,6 +249,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; }; @@ -266,8 +263,6 @@ CODE_SIGN_ENTITLEMENTS = "nfc-demo/nfc-demo.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"nfc-demo/Preview Content\""; - DEVELOPMENT_TEAM = 5YYXE2JG54; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "nfc-demo/Info.plist"; @@ -298,8 +293,6 @@ CODE_SIGN_ENTITLEMENTS = "nfc-demo/nfc-demo.entitlements"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"nfc-demo/Preview Content\""; - DEVELOPMENT_TEAM = 5YYXE2JG54; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "nfc-demo/Info.plist"; diff --git a/mvoting-nfc/nfc-demo/Operator.swift b/mvoting-nfc/nfc-demo/Operator.swift index 826e0c6..a9ad109 100644 --- a/mvoting-nfc/nfc-demo/Operator.swift +++ b/mvoting-nfc/nfc-demo/Operator.swift @@ -24,11 +24,10 @@ extension Operator: CardOperations { do { let result = try await OperationReadPublicData().startReading(CAN: CAN) return result + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } @@ -36,11 +35,10 @@ extension Operator: CardOperations { do { let cert = try await OperationReadCertificate().startReading(CAN: CAN, certUsage: .auth) return cert + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } @@ -48,23 +46,21 @@ extension Operator: CardOperations { do { let cert = try await OperationReadCertificate().startReading(CAN: CAN, certUsage: .sign) return cert + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } public func loadWebEIDAuthenticationData(CAN: String, pin1: SecureData, challenge: String, origin: String) async throws -> WebEidData { do { - let webEidData = try await OperationAuthenticateWithWebEID(CAN: CAN, pin1: pin1, challenge: challenge, origin: origin).startReading() + let webEidData = try await OperationAuthenticateWithWebEID().startReading(CAN: CAN, pin1: pin1, challenge: challenge, origin: origin) return webEidData + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } @@ -72,33 +68,30 @@ extension Operator: CardOperations { do { let signature = try await OperationSignHash().startSigning(CAN: CAN, PIN2: pin2, hash: hash) return signature + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } public func unblockPin1(CAN: String, puk: SecureData, newCode: SecureData) async throws { do { try await OperationUnblockPin().startReading(CAN: CAN, codeType: .pin1, puk: puk, newPin: newCode) + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } public func unblockPin2(CAN: String, puk: SecureData, newCode: SecureData) async throws { do { try await OperationUnblockPin().startReading(CAN: CAN, codeType: .pin2, puk: puk, newPin: newCode) + } catch let error as IdCardInternalError { + throw error.getIdCardError() } catch { - guard let e = error as? IdCardInternalError else { - throw IdCardError.sessionError - } - throw e.getIdCardError() + throw IdCardError.sessionError } } } diff --git a/mvoting-nfc/nfc-demo/Preview Content/Preview Assets.xcassets/Contents.json b/mvoting-nfc/nfc-demo/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/mvoting-nfc/nfc-demo/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -}