From dbfcc96fe324d854b050e322d661bade74777d74 Mon Sep 17 00:00:00 2001 From: Gematik <52454541+gematik1@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:57:48 +0100 Subject: [PATCH] Version 5.6.0 (#19) --- .github/README.adoc | 103 ++++- .gitignore | 2 +- .../PublisherIntegrationTest.swift | 26 +- .../AuthenticateChallengeE256Test.swift | 2 +- .../AuthenticateChallengeR2048Test.swift | 2 +- ...ChannelTypeExtVersionIntegrationTest.swift | 8 +- .../DetermineCardAidIntegrationTest.swift | 2 +- ...ealthCardTypeExtESIGNIntegrationTest.swift | 2 +- ...HealthCardTypeExtEfCardAccessIntTest.swift | 2 +- ...eExtResetRetryCounterIntegrationTest.swift | 4 +- ...tRetryCounterIntegrationTestContCont.swift | 4 +- .../HealthCardTypeExtVerifyPinTest.swift | 4 +- .../OpenSecureSessionIntegrationTest.swift | 2 +- .../ReadAutCertificateE256Test.swift | 2 +- .../ReadAutCertificateR2048Test.swift | 2 +- .../ReadFileIntegrationTest.swift | 20 +- .../SelectCommandIntegrationTest.swift | 8 +- ...ithFCPOnSecureChannelIntegrationTest.swift | 4 +- Mintfile | 2 +- Package.swift | 8 +- ReleaseNotes.md | 10 + .../Card/CardChannelType.swift | 2 + .../CardReaderProviderApi/Card/CardType.swift | 1 + .../HealthCardCommandType.swift | 6 +- .../HealthCardType+Authenticate.swift | 6 +- .../HealthCardType+ChangeReferenceData.swift | 2 +- .../Authentication/HealthCardType+ESIGN.swift | 14 +- .../HealthCardType+ResetRetryCounter.swift | 10 +- .../HealthCardType+VerifyPin.swift | 8 +- .../Operations/CardChannelType+CardAID.swift | 4 +- .../CardChannelType+CardAccess.swift | 8 +- .../Operations/CardChannelType+Version.swift | 8 +- .../Operations/HealthCardType+ReadFile.swift | 12 +- .../CardType+SecureMessaging.swift | 37 +- .../SecureMessaging/KeyAgreement.swift | 32 +- .../SecureMessaging/SecureCardChannel.swift | 1 + .../Card/NFCCardChannel.swift | 9 +- .../NFCHealthCardSession.swift | 399 ++++++++++++++++++ .../NFCTagReaderSession+Publisher.swift | 1 + Sources/NFCDemo/CallOnMainThread.swift | 32 -- .../HealthCardControl+LocalizedError.swift | 4 +- ...NFCCardReaderProvider+LocalizedError.swift | 43 +- .../NFCChangeReferenceDataController.swift | 138 ++---- Sources/NFCDemo/NFC/NFCLoginController.swift | 190 ++++----- .../NFC/NFCResetRetryCounterController.swift | 253 ++++------- .../Resources/Base.lproj/Localizable.strings | 1 + Sources/NFCDemo/Resources/Info.plist | 2 +- .../Resources/de.lproj/Localizable.strings | 1 + .../NFCChangeReferenceDataViewModel.swift | 12 +- .../Registration/NFCLoginViewModel.swift | 12 +- .../NFCResetRetryCounterViewModel.swift | 18 +- .../Screens/Registration/StartNFCView.swift | 26 +- .../HealthCardTypeExtESIGNTest.swift | 16 +- doc/userguide/OHCKIT_GettingStarted.adoc | 4 +- doc/userguide/OHCKIT_HealthCardAccess.adoc | 12 +- doc/userguide/OHCKIT_HealthCardControl.adoc | 4 +- .../OHCKIT_NFCCardReaderProvider.adoc | 28 +- project.yml | 1 + scripts/bootstrap | 4 + 59 files changed, 981 insertions(+), 599 deletions(-) create mode 100644 Sources/NFCCardReaderProvider/NFCHealthCardSession.swift delete mode 100644 Sources/NFCDemo/CallOnMainThread.swift diff --git a/.github/README.adoc b/.github/README.adoc index fdd1c2b..c4831b6 100644 --- a/.github/README.adoc +++ b/.github/README.adoc @@ -26,13 +26,13 @@ This document describes the functionalitiy and structure of OpenHealthCardKit. Generated API docs are available at https://gematik.github.io/ref-OpenHealthCardKit. == Getting Started -OpenHealthCardKit requires Swift 5.1. +OpenHealthCardKit requires Swift 5.6. === Setup for integration - **Swift Package Manager:** Put this in your `Package.swift`: - `.package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.3.0"),` + `.package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.6.0"),` - **Carthage:** Put this in your `Cartfile`: @@ -116,29 +116,30 @@ let eSign = EgkFileSystem.DF.ESIGN let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid) ---- -===== Setting an execution target +===== Command execution We execute the created command `CardType` instance which has been typically provided by a `CardReaderType`. In the next example we use a `HealthCard` object representing an eGK (elektronische Gesundheitskarte) -as one kind of a `HealthCardType` implementing the `CardType` protocol. - +as one kind of a `HealthCardType` implementing the `CardType` protocol and then send the command to the card (or card's channel): [source,swift] ---- -// initialize your CardReaderType instance -let cardReader: CardReaderType = CardSimulationTerminalTestCase.reader -let card = try cardReader.connect([:])! -let healthCardStatus = HealthCardStatus.valid(cardType: .egk(generation: .g2)) -let eGk = try HealthCard(card: card, status: healthCardStatus) -let publisher: AnyPublisher = selectEsignCommand.publisher(for: eGk) +let healthCardResponse = try await selectEsignCommand.transmit(to: Self.healthCard) +guard healthCardResponse.responseStatus == ResponseStatus.success else { + throw HealthCard.Error.operational // TO-DO: handle this or throw a meaningful Error +} ---- + +*Following paragraphs describe the deprecated way of executung commands via the _Combine_ inteface:* + A created command can be lifted to the Combine framework with `publisher(for:writetimeout:readtimeout)`. The result of the command execution can be validated against an expected `ResponseStatus`, e.g. +SUCCESS+ (+0x9000+). [source,swift] ---- +let publisher: AnyPublisher = selectEsignCommand.publisher(for: eGk) let checkResponse = publisher.tryMap { healthCardResponse -> HealthCardResponseType in guard healthCardResponse.responseStatus == ResponseStatus.success else { throw HealthCard.Error.operational // throw a meaningful Error @@ -219,17 +220,11 @@ Take the necessary preparatory steps for signing a challenge on the Health Card, [source,swift] ---- -expect { - let challenge = Data([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]) - let format2Pin = try Format2Pin(pincode: "123456") - return try Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) - .flatMap { _ in - Self.healthCard.sign(data: challenge) - } - .eraseToAnyPublisher() - .test() - .responseStatus -} == ResponseStatus.success +let challenge = Data([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]) +let format2Pin = try Format2Pin(pincode: "123456") +_ = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) +let signResponse = try await Self.healthCard.sign(data: challenge) +expect(signResponse.responseStatus) == ResponseStatus.success ---- @@ -238,7 +233,7 @@ steps for establishing a secure channel with the Health Card and expose only a s [source,swift] ---- -try KeyAgreement.Algorithm.idPaceEcdhGmAesCbcCmac128.negotiateSessionKey( +let secureMessaging = try await KeyAgreement.Algorithm.idPaceEcdhGmAesCbcCmac128.negotiateSessionKey( card: CardSimulationTerminalTestCase.healthCard, can: can, writeTimeout: 0, @@ -253,6 +248,68 @@ for more already implemented use cases. A `CardReaderProvider` implementation that handles the communication with the Apple iPhone NFC interface. + +==== NFCCardReaderSession + +For convience, the `NFCCardReaderSession` combines the usage of the NFC inteface with the `HealthCardAccess/HealthCardControl` layers. + +The initializer takes some NFC-Display messages, the CAN (card access number) and a closure with a `NFCHealthCardSessionHandle` to send/receive commands/responses to/from the NFC HealthCard and to update the user's interface message to. + +[source,swift] +---- +guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_verify_pin", comment: "")) + let verifyPinResponse = try await session.card.verifyAsync( + pin: format2Pin, + type: EgkFileSystem.Pin.mrpinHome + ) + if case let VerifyPinResponse.wrongSecretWarning(retryCount: count) = verifyPinResponse { + throw NFCLoginController.Error.wrongPin(retryCount: count) + } else if case VerifyPinResponse.passwordBlocked = verifyPinResponse { + throw NFCLoginController.Error.passwordBlocked + } else if VerifyPinResponse.success != verifyPinResponse { + throw NFCLoginController.Error.verifyPinResponse + } + + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_signing", comment: "")) + let outcome = try await session.card.sign( + payload: "ABC".data(using: .utf8)!, // swiftlint:disable:this force_unwrapping + checkAlgorithm: checkBrainpoolAlgorithm + ) + + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_success", comment: "")) + return outcome +}) +else { + // handle the case the Session could not be initialized +---- + +Execute the operation on the NFC HealthCard. The secure channel (PACE) is established initially before executing the operation. + +[source,swift] +---- +signedData = try await nfcHealthCardSession.executeOperation() +---- + +The thrown error will be of type `NFCHealthCardSessionError`. +The `NFCHealthCardSession` also gives you an endpoint to invalidate the underlying `TagReaderSession`. + +[source,swift] +---- +} catch NFCHealthCardSessionError.coreNFC(.userCanceled) { + // error type is always `NFCHealthCardSessionError` + // here we especially handle when the user canceled the session + Task { @MainActor in self.pState = .idle } // Do some view-property update + // Calling .invalidateSession() is not strictly necessary + // since nfcHealthCardSession does it while it's de-initializing. + nfcHealthCardSession.invalidateSession(with: nil) + return +} catch { + Task { @MainActor in self.pState = .error(error) } + nfcHealthCardSession.invalidateSession(with: error.localizedDescription) + return +} +---- [#NFCDemo] === NFCDemo diff --git a/.gitignore b/.gitignore index b948d33..ecb5a1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .DS_Store -/.build +/.build /Packages .vscode .swiftpm diff --git a/IntegrationTests/HealthCardAccess/PublisherIntegrationTest.swift b/IntegrationTests/HealthCardAccess/PublisherIntegrationTest.swift index 1fc89a3..e9f5f01 100644 --- a/IntegrationTests/HealthCardAccess/PublisherIntegrationTest.swift +++ b/IntegrationTests/HealthCardAccess/PublisherIntegrationTest.swift @@ -54,31 +54,43 @@ final class PublisherIntegrationTest: CardSimulationTerminalTestCase { } == ResponseStatus.endOfFileWarning } - // swiftlint:disable force_unwrapping - func codeForUserManual() { + func testCodeForUserManual() async throws { // tag::createCommand[] let eSign = EgkFileSystem.DF.ESIGN let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid) // end::createCommand[] + // tag::evaluateResponseStatus[] + let healthCardResponse = try await selectEsignCommand.transmitAsync(to: Self.healthCard) + guard healthCardResponse.responseStatus == ResponseStatus.success else { + throw HealthCard.Error.operational // TO-DO: handle this or throw a meaningful Error + } + // end::evaluateResponseStatus[] + + // expect that no error has been thrown + } + + // swiftlint:disable force_unwrapping + func codeForUserManual_publisher() { + let eSign = EgkFileSystem.DF.ESIGN + let selectEsignCommand = HealthCardCommand.Select.selectFile(with: eSign.aid) + expect { - // tag::setExecutionTarget[] // initialize your CardReaderType instance let cardReader: CardReaderType = CardSimulationTerminalTestCase.reader let card = try cardReader.connect([:])! let healthCardStatus = HealthCardStatus.valid(cardType: .egk(generation: .g2)) let eGk = try HealthCard(card: card, status: healthCardStatus) - let publisher: AnyPublisher = selectEsignCommand.publisher(for: eGk) - // end::setExecutionTarget[] - // tag::evaluateResponseStatus[] + // tag::evaluateResponseStatus_publisher[] + let publisher: AnyPublisher = selectEsignCommand.publisher(for: eGk) let checkResponse = publisher.tryMap { healthCardResponse -> HealthCardResponseType in guard healthCardResponse.responseStatus == ResponseStatus.success else { throw HealthCard.Error.operational // throw a meaningful Error } return healthCardResponse } - // end::evaluateResponseStatus[] + // end::evaluateResponseStatus_publisher[] // tag::createCommandSequence[] let readCertificate = checkResponse diff --git a/IntegrationTests/HealthCardControl/AuthenticateChallengeE256Test.swift b/IntegrationTests/HealthCardControl/AuthenticateChallengeE256Test.swift index 8fb72b2..dda87e6 100644 --- a/IntegrationTests/HealthCardControl/AuthenticateChallengeE256Test.swift +++ b/IntegrationTests/HealthCardControl/AuthenticateChallengeE256Test.swift @@ -48,7 +48,7 @@ final class AuthenticateChallengeE256Test: CardSimulationTerminalTestCase { let challenge = "1234567890".data(using: .utf8)! _ = try await Self.healthCard .verify(pin: "123456", type: .mrpinHome) - let authenticatedResult = try await Self.healthCard.authenticate(challenge: challenge) + let authenticatedResult = try await Self.healthCard.authenticateAsync(challenge: challenge) expect(authenticatedResult.certificate.signatureAlgorithm) == .ecdsaSha256 expect(authenticatedResult.certificate.certificate.count) == 885 diff --git a/IntegrationTests/HealthCardControl/AuthenticateChallengeR2048Test.swift b/IntegrationTests/HealthCardControl/AuthenticateChallengeR2048Test.swift index f003811..b049cd9 100644 --- a/IntegrationTests/HealthCardControl/AuthenticateChallengeR2048Test.swift +++ b/IntegrationTests/HealthCardControl/AuthenticateChallengeR2048Test.swift @@ -42,7 +42,7 @@ final class AuthenticateChallengeR2048Test: CardSimulationTerminalTestCase { let challenge = "1234567890".data(using: .utf8)! _ = try await Self.healthCard .verify(pin: "123456", type: .mrpinHome) - let authenticatedResult = try await Self.healthCard.authenticate(challenge: challenge) + let authenticatedResult = try await Self.healthCard.authenticateAsync(challenge: challenge) expect(authenticatedResult.certificate.signatureAlgorithm) == .sha256RsaMgf1 expect(authenticatedResult.certificate.certificate.count) == 1242 diff --git a/IntegrationTests/HealthCardControl/CardChannelTypeExtVersionIntegrationTest.swift b/IntegrationTests/HealthCardControl/CardChannelTypeExtVersionIntegrationTest.swift index d279080..cbe02ea 100644 --- a/IntegrationTests/HealthCardControl/CardChannelTypeExtVersionIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/CardChannelTypeExtVersionIntegrationTest.swift @@ -35,7 +35,7 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest } func testReadCardTypeFromVersion() async throws { - let cardType = try await Self.healthCard.currentCardChannel.readCardType() + let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync() expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1) } @@ -51,8 +51,8 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest } func testDetermineCardAidThenReadCardTypeFromVersion() async throws { - let cardAid = try await Self.healthCard.currentCardChannel.determineCardAid() - let cardType = try await Self.healthCard.currentCardChannel.readCardType(cardAid: cardAid) + let cardAid = try await Self.healthCard.currentCardChannel.determineCardAidAsync() + let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync(cardAid: cardAid) expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1) } @@ -66,7 +66,7 @@ final class CardChannelTypeExtVersionIntegrationTest: CardSimulationTerminalTest func testReadCardTypeFromVersionWithKnownCardAid() async throws { let cardAid = CardAid.egk - let cardType = try await Self.healthCard.currentCardChannel.readCardType(cardAid: cardAid) + let cardType = try await Self.healthCard.currentCardChannel.readCardTypeAsync(cardAid: cardAid) expect(cardType) == HealthCardPropertyType.egk(generation: .g2_1) } } diff --git a/IntegrationTests/HealthCardControl/DetermineCardAidIntegrationTest.swift b/IntegrationTests/HealthCardControl/DetermineCardAidIntegrationTest.swift index bab335c..bd2e859 100644 --- a/IntegrationTests/HealthCardControl/DetermineCardAidIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/DetermineCardAidIntegrationTest.swift @@ -33,7 +33,7 @@ final class DetermineCardAidIntegrationTest: CardSimulationTerminalTestCase { } func testDetermineCardAid() async throws { - let result = try await Self.healthCard.currentCardChannel.determineCardAid() + let result = try await Self.healthCard.currentCardChannel.determineCardAidAsync() expect(result) == CardAid.egk } } diff --git a/IntegrationTests/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift b/IntegrationTests/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift index 2203a8a..816112e 100644 --- a/IntegrationTests/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift @@ -49,7 +49,7 @@ final class HealthCardTypeExtESIGNIntegrationTest: CardSimulationTerminalTestCas let challenge = Data([0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]) let format2Pin = try Format2Pin(pincode: "123456") _ = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) - let signResponse = try await Self.healthCard.sign(data: challenge) + let signResponse = try await Self.healthCard.signAsync(data: challenge) expect(signResponse.responseStatus) == ResponseStatus.success // end::signChallenge[] } diff --git a/IntegrationTests/HealthCardControl/HealthCardTypeExtEfCardAccessIntTest.swift b/IntegrationTests/HealthCardControl/HealthCardTypeExtEfCardAccessIntTest.swift index 17534d9..98a4a36 100644 --- a/IntegrationTests/HealthCardControl/HealthCardTypeExtEfCardAccessIntTest.swift +++ b/IntegrationTests/HealthCardControl/HealthCardTypeExtEfCardAccessIntTest.swift @@ -32,7 +32,7 @@ final class HealthCardTypeExtEfCardAccessIntTest: CardSimulationTerminalTestCase } func testReadEfCardAccess() async throws { - let algorithm = try await Self.healthCard.currentCardChannel.readKeyAgreementAlgorithm( + let algorithm = try await Self.healthCard.currentCardChannel.readKeyAgreementAlgorithmAsync( writeTimeout: 30, readTimeout: 30 ) diff --git a/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTest.swift b/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTest.swift index 5febdbd..2b7c381 100644 --- a/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTest.swift @@ -71,7 +71,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTest: CardSimulationTer let puk = "12345678" as Format2Pin let newPin = "654321" as Format2Pin - let response = try await Self.healthCard.resetRetryCounterAndSetNewPin( + let response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync( puk: puk, newPin: newPin, type: EgkFileSystem.Pin.mrpinHome, @@ -99,7 +99,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTest: CardSimulationTer let puk = "12345678" as Format2Pin let tooLongNewPin = "654112341234" as Format2Pin - let response = try await Self.healthCard.resetRetryCounterAndSetNewPin( + let response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync( puk: puk, newPin: tooLongNewPin, type: EgkFileSystem.Pin.mrpinHome, diff --git a/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTestContCont.swift b/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTestContCont.swift index 0b2f76d..d5f5813 100644 --- a/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTestContCont.swift +++ b/IntegrationTests/HealthCardControl/HealthCardTypeExtResetRetryCounterIntegrationTestContCont.swift @@ -37,7 +37,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTestContCont: CardSimul // With setting a new PIN var response: ResetRetryCounterResponse - response = try await Self.healthCard.resetRetryCounterAndSetNewPin( + response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync( puk: wrongPuk, newPin: newPin, type: EgkFileSystem.Pin.mrpinHome, @@ -45,7 +45,7 @@ final class HealthCardTypeExtResetRetryCounterIntegrationTestContCont: CardSimul ) expect(response) == ResetRetryCounterResponse.wrongSecretWarning(retryCount: 9) - response = try await Self.healthCard.resetRetryCounterAndSetNewPin( + response = try await Self.healthCard.resetRetryCounterAndSetNewPinAsync( puk: wrongPuk, newPin: newPin, type: EgkFileSystem.Pin.mrpinHome, diff --git a/IntegrationTests/HealthCardControl/HealthCardTypeExtVerifyPinTest.swift b/IntegrationTests/HealthCardControl/HealthCardTypeExtVerifyPinTest.swift index b133f5a..0142064 100644 --- a/IntegrationTests/HealthCardControl/HealthCardTypeExtVerifyPinTest.swift +++ b/IntegrationTests/HealthCardControl/HealthCardTypeExtVerifyPinTest.swift @@ -40,7 +40,7 @@ final class HealthCardTypeExtVerifyPinTest: CardSimulationTerminalTestCase { func testVerifyMrPinHomeEgk21() async throws { let pinCode = "123456" let format2Pin = try Format2Pin(pincode: pinCode) - let response = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) + let response = try await Self.healthCard.verifyAsync(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) expect(response) == VerifyPinResponse.success } @@ -56,7 +56,7 @@ final class HealthCardTypeExtVerifyPinTest: CardSimulationTerminalTestCase { func testVerifyMrPinHomeEgk21_WarningRetryCounter() async throws { let pinCode = "654321" let format2Pin = try Format2Pin(pincode: pinCode) - let response = try await Self.healthCard.verify(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) + let response = try await Self.healthCard.verifyAsync(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome) // Note: The retry counter is not reset after each test case. Therefore, the retry counter is 1 here. expect(response) == VerifyPinResponse.wrongSecretWarning(retryCount: 1) } diff --git a/IntegrationTests/HealthCardControl/OpenSecureSessionIntegrationTest.swift b/IntegrationTests/HealthCardControl/OpenSecureSessionIntegrationTest.swift index b8c19e1..bcf7bc9 100644 --- a/IntegrationTests/HealthCardControl/OpenSecureSessionIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/OpenSecureSessionIntegrationTest.swift @@ -41,7 +41,7 @@ final class OpenSecureSessionIntegrationTest: CardSimulationTerminalTestCase { func testOpenSecureSession() async throws { let can = try! CAN.from(Data("123123".utf8)) // swiftlint:disable:this force_try - let secureHealthCard = try await Self.card.openSecureSession(can: can, writeTimeout: 0, readTimeout: 0) + let secureHealthCard = try await Self.card.openSecureSessionAsync(can: can, writeTimeout: 0, readTimeout: 0) expect(secureHealthCard.status.type) == .egk(generation: .g2) } } diff --git a/IntegrationTests/HealthCardControl/ReadAutCertificateE256Test.swift b/IntegrationTests/HealthCardControl/ReadAutCertificateE256Test.swift index 07d175c..c45624d 100644 --- a/IntegrationTests/HealthCardControl/ReadAutCertificateE256Test.swift +++ b/IntegrationTests/HealthCardControl/ReadAutCertificateE256Test.swift @@ -53,7 +53,7 @@ final class ReadAutCertificateE256Test: CardSimulationTerminalTestCase { func testReadAutCertificateE256() async throws { var autCertificateResponse: AutCertificateResponse? - autCertificateResponse = try await Self.healthCard.readAutCertificate() + autCertificateResponse = try await Self.healthCard.readAutCertificateAsync() expect(autCertificateResponse?.info) == .efAutE256 expect(autCertificateResponse?.certificate) == expectedCertificate diff --git a/IntegrationTests/HealthCardControl/ReadAutCertificateR2048Test.swift b/IntegrationTests/HealthCardControl/ReadAutCertificateR2048Test.swift index d296d43..6ebb8e2 100644 --- a/IntegrationTests/HealthCardControl/ReadAutCertificateR2048Test.swift +++ b/IntegrationTests/HealthCardControl/ReadAutCertificateR2048Test.swift @@ -50,7 +50,7 @@ final class ReadAutCertificateR2048Test: CardSimulationTerminalTestCase { func testReadAutCertificate2048() async throws { var autCertificateResponse: AutCertificateResponse? autCertificateResponse = try await CardSimulationTerminalTestCase.healthCard - .readAutCertificate() + .readAutCertificateAsync() expect(autCertificateResponse?.info) == .efAutR2048 expect(autCertificateResponse?.certificate) == expectedCertificate } diff --git a/IntegrationTests/HealthCardControl/ReadFileIntegrationTest.swift b/IntegrationTests/HealthCardControl/ReadFileIntegrationTest.swift index e0c9d84..4abe4ae 100644 --- a/IntegrationTests/HealthCardControl/ReadFileIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/ReadFileIntegrationTest.swift @@ -71,10 +71,10 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { } func testReadFileTillEOF() async throws { - let (responseStatus, _) = try await Self.healthCard.selectDedicated(file: dedicatedFile) + let (responseStatus, _) = try await Self.healthCard.selectDedicatedAsync(file: dedicatedFile) expect(responseStatus) == ResponseStatus.success - let readData = try await Self.healthCard.readSelectedFile(expected: nil, failOnEndOfFileWarning: false) + let readData = try await Self.healthCard.readSelectedFileAsync(expected: nil, failOnEndOfFileWarning: false) expect(readData) == expectedCertificate } @@ -92,7 +92,7 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { } func testReadFileFailOnEOF() async throws { - let (responseStatus, _) = try await Self.healthCard.selectDedicated(file: dedicatedFile) + let (responseStatus, _) = try await Self.healthCard.selectDedicatedAsync(file: dedicatedFile) expect(responseStatus) == ResponseStatus.success // todo-nimble update @@ -120,12 +120,12 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { } func testReadFile() async throws { - let (responseStatus, fcp) = try await Self.healthCard.selectDedicated(file: dedicatedFile, fcp: true) + let (responseStatus, fcp) = try await Self.healthCard.selectDedicatedAsync(file: dedicatedFile, fcp: true) expect(responseStatus) == .success expect(fcp).toNot(beNil()) // swiftlint:disable:next force_unwrapping - let readData = try await Self.healthCard.readSelectedFile( + let readData = try await Self.healthCard.readSelectedFileAsync( expected: Int(fcp!.readSize!), failOnEndOfFileWarning: true ) @@ -169,11 +169,11 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { return } - let (responseStatus, fcp) = try await healthCard.selectDedicated(file: dedicatedFile, fcp: true) + let (responseStatus, fcp) = try await healthCard.selectDedicatedAsync(file: dedicatedFile, fcp: true) expect(responseStatus) == .success expect(fcp).toNot(beNil()) - let readData = try await healthCard.readSelectedFile( + let readData = try await healthCard.readSelectedFileAsync( expected: Int(fcp!.readSize!), failOnEndOfFileWarning: true ) @@ -216,11 +216,11 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { return } - let (responseStatus, fcp) = try await healthCard.selectDedicated(file: dedicatedFile, fcp: true) + let (responseStatus, fcp) = try await healthCard.selectDedicatedAsync(file: dedicatedFile, fcp: true) expect(responseStatus) == .success expect(fcp).toNot(beNil()) - let readData = try await healthCard.readSelectedFile(expected: nil, failOnEndOfFileWarning: false) + let readData = try await healthCard.readSelectedFileAsync(expected: nil, failOnEndOfFileWarning: false) expect(readData) == expectedCertificate } @@ -258,7 +258,7 @@ final class ReadFileIntegrationTest: CardSimulationTerminalTestCase { return } - let (responseStatus, _) = try await healthCard.selectDedicated(file: dedicatedFile) + let (responseStatus, _) = try await healthCard.selectDedicatedAsync(file: dedicatedFile) expect(responseStatus) == ResponseStatus.success // todo-nimble update diff --git a/IntegrationTests/HealthCardControl/SelectCommandIntegrationTest.swift b/IntegrationTests/HealthCardControl/SelectCommandIntegrationTest.swift index 1aa5048..22d4fdf 100644 --- a/IntegrationTests/HealthCardControl/SelectCommandIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/SelectCommandIntegrationTest.swift @@ -23,17 +23,17 @@ import XCTest final class SelectCommandIntegrationTest: CardSimulationTerminalTestCase { func testSelectRoot() async throws { let selectRootCommand = HealthCardCommand.Select.selectRoot() - let selectRootResponse = try await selectRootCommand.transmit(to: Self.healthCard) + let selectRootResponse = try await selectRootCommand.transmitAsync(to: Self.healthCard) expect(selectRootResponse.responseStatus) == ResponseStatus.success } func testSelectFileByAidThenSelectParentFolder() async throws { let selectFileCommand = HealthCardCommand.Select.selectFile(with: EgkFileSystem.DF.GDD.aid) - let selectFileResponse = try await selectFileCommand.transmit(to: Self.healthCard) + let selectFileResponse = try await selectFileCommand.transmitAsync(to: Self.healthCard) expect(selectFileResponse.responseStatus) == ResponseStatus.success let selectRootCommand = HealthCardCommand.Select.selectRoot() - let selectRootResponse = try await selectRootCommand.transmit(to: Self.healthCard) + let selectRootResponse = try await selectRootCommand.transmitAsync(to: Self.healthCard) expect(selectRootResponse.responseStatus) == ResponseStatus.success } @@ -45,7 +45,7 @@ final class SelectCommandIntegrationTest: CardSimulationTerminalTestCase { ne: cEgkAutCVCE256Count + 1, offset: 0 ) - let readFileResponse = try await readFileCommand.transmit(to: Self.healthCard) + let readFileResponse = try await readFileCommand.transmitAsync(to: Self.healthCard) expect(readFileResponse.responseStatus) == ResponseStatus.endOfFileWarning } } diff --git a/IntegrationTests/HealthCardControl/SelectWithFCPOnSecureChannelIntegrationTest.swift b/IntegrationTests/HealthCardControl/SelectWithFCPOnSecureChannelIntegrationTest.swift index d4ced56..1198269 100644 --- a/IntegrationTests/HealthCardControl/SelectWithFCPOnSecureChannelIntegrationTest.swift +++ b/IntegrationTests/HealthCardControl/SelectWithFCPOnSecureChannelIntegrationTest.swift @@ -46,8 +46,8 @@ final class SelectWithFCPOnSecureChannelIntegrationTest: CardSimulationTerminalT func testSelectEsignCChAutE256WithFCP() async throws { let can = try CAN.from(Data("123123".utf8)) - let secureCard = try await Self.card.openSecureSession(can: can, writeTimeout: 0, readTimeout: 0) - let response = try await secureCard.selectDedicated( + let secureCard = try await Self.card.openSecureSessionAsync(can: can, writeTimeout: 0, readTimeout: 0) + let response = try await secureCard.selectDedicatedAsync( file: DedicatedFile(aid: EgkFileSystem.DF.ESIGN.aid, fid: EgkFileSystem.EF.esignCChAutE256.fid), fcp: true ) diff --git a/Mintfile b/Mintfile index 4be47dc..7b2b7fe 100644 --- a/Mintfile +++ b/Mintfile @@ -1,4 +1,4 @@ -realm/SwiftLint@0.43.1 +realm/SwiftLint@0.44.0 yonaskolb/xcodegen@2.38.0 Carthage/Carthage@0.39.0 nicklockwood/SwiftFormat@0.48.7 \ No newline at end of file diff --git a/Package.swift b/Package.swift index ac825d2..b2f5ec0 100644 --- a/Package.swift +++ b/Package.swift @@ -34,7 +34,13 @@ let package = Package( targets: [ .target( name: "NFCCardReaderProvider", - dependencies: ["HealthCardAccess", "Helper", .product(name: "GemCommonsKit", package: "ref-GemCommonsKit"), "DataKit"] + dependencies: [ + "HealthCardControl", + "HealthCardAccess", + "Helper", + .product(name: "GemCommonsKit", package: "ref-GemCommonsKit"), + "DataKit" + ] ), .target( name: "HealthCardControl", diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5df4761..bb83c5f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,13 @@ +# Release 5.6.0 + +## Added + +- New NFCHealthCardSession offers a structured concurrency API + +## Other + +- adapt app target NFCDemo to use the new NFCHealthCardSession API + # Release 5.5.0 ## Added diff --git a/Sources/CardReaderProviderApi/Card/CardChannelType.swift b/Sources/CardReaderProviderApi/Card/CardChannelType.swift index b0e42aa..63d43aa 100644 --- a/Sources/CardReaderProviderApi/Card/CardChannelType.swift +++ b/Sources/CardReaderProviderApi/Card/CardChannelType.swift @@ -49,6 +49,7 @@ public protocol CardChannelType { - Returns: the Command APDU Response or CardError on failure */ + @_disfavoredOverload @available(*, deprecated, message: "Use structured concurrency version instead") func transmit(command: CommandType, writeTimeout: TimeInterval, readTimeout: TimeInterval) throws -> ResponseType @@ -74,6 +75,7 @@ public protocol CardChannelType { - Throws `CardError` */ + @_disfavoredOverload func close() throws /** diff --git a/Sources/CardReaderProviderApi/Card/CardType.swift b/Sources/CardReaderProviderApi/Card/CardType.swift index 3afd86f..9a7246d 100644 --- a/Sources/CardReaderProviderApi/Card/CardType.swift +++ b/Sources/CardReaderProviderApi/Card/CardType.swift @@ -47,6 +47,7 @@ public protocol CardType { - Returns: The (connected) card channel */ + @_disfavoredOverload @available(*, deprecated, message: "Use structured concurrency version instead") func openLogicChannel() throws -> CardChannelType diff --git a/Sources/HealthCardAccess/HealthCardCommandType.swift b/Sources/HealthCardAccess/HealthCardCommandType.swift index ad87a6c..ab54c5e 100644 --- a/Sources/HealthCardAccess/HealthCardCommandType.swift +++ b/Sources/HealthCardAccess/HealthCardCommandType.swift @@ -56,12 +56,12 @@ extension HealthCardCommandType { /// - writeTimeout: the time in seconds to allow for the write to begin. time <= 0 no timeout /// - readTimeout: the time in seconds to allow for the receiving to begin. time <= 0 no timeout /// - Returns: HealthCardResponseType decoded from the answer received via channel. - public func transmit( + public func transmitAsync( to card: HealthCardType, writeTimeout: TimeInterval = 0, readTimeout: TimeInterval = 0 ) async throws -> HealthCardResponseType { - try await transmit(on: card.currentCardChannel, writeTimeout: writeTimeout, readTimeout: readTimeout) + try await transmitAsync(on: card.currentCardChannel, writeTimeout: writeTimeout, readTimeout: readTimeout) } /// Execute the command on a given channel @@ -98,7 +98,7 @@ extension HealthCardCommandType { /// - writeTimeout: the time in seconds to allow for the write to begin. time <= 0 no timeout /// - readTimeout: the time in seconds to allow for the receiving to begin. time <= 0 no timeout /// - Returns: HealthCardResponseType decoded from the answer received via channel. - public func transmit( + public func transmitAsync( on channel: CardChannelType, writeTimeout: TimeInterval = 0, readTimeout: TimeInterval = 0 diff --git a/Sources/HealthCardControl/Authentication/HealthCardType+Authenticate.swift b/Sources/HealthCardControl/Authentication/HealthCardType+Authenticate.swift index 430124a..a6d54d6 100644 --- a/Sources/HealthCardControl/Authentication/HealthCardType+Authenticate.swift +++ b/Sources/HealthCardControl/Authentication/HealthCardType+Authenticate.swift @@ -53,10 +53,10 @@ extension HealthCardType { /// /// - Parameter challenge: the data to sign /// - Returns: AuthenticationResult (Aut certificate and its signature method and DSA signed challenge data) - public func authenticate(challenge: Data) async throws -> AuthenticationResult { - let autCertificate = try await readAutCertificate() + public func authenticateAsync(challenge: Data) async throws -> AuthenticationResult { + let autCertificate = try await readAutCertificateAsync() let certificateInfo = try autCertificate.certificateInfo() - let signResponse = try await sign(data: challenge) + let signResponse = try await signAsync(data: challenge) guard signResponse.responseStatus == .success, let signatureData = signResponse.data else { diff --git a/Sources/HealthCardControl/Authentication/HealthCardType+ChangeReferenceData.swift b/Sources/HealthCardControl/Authentication/HealthCardType+ChangeReferenceData.swift index 956d3c9..f99ade4 100644 --- a/Sources/HealthCardControl/Authentication/HealthCardType+ChangeReferenceData.swift +++ b/Sources/HealthCardControl/Authentication/HealthCardType+ChangeReferenceData.swift @@ -107,7 +107,7 @@ extension HealthCardType { CommandLogger.commands.append(Command(message: "Change Reference Data: Set New PIN", type: .description)) let parameters = (password: type.rawValue, dfSpecific: dfSpecific, old: old, new: new) let changeReferenceDataCommand = try HealthCardCommand.ChangeReferenceData.change(password: parameters) - let changeReferenceDataResponse = try await changeReferenceDataCommand.transmit(to: self) + let changeReferenceDataResponse = try await changeReferenceDataCommand.transmitAsync(to: self) let responseStatus = changeReferenceDataResponse.responseStatus if ResponseStatus.wrongSecretWarnings.contains(responseStatus) { diff --git a/Sources/HealthCardControl/Authentication/HealthCardType+ESIGN.swift b/Sources/HealthCardControl/Authentication/HealthCardType+ESIGN.swift index 6c89c21..dc0cf87 100644 --- a/Sources/HealthCardControl/Authentication/HealthCardType+ESIGN.swift +++ b/Sources/HealthCardControl/Authentication/HealthCardType+ESIGN.swift @@ -101,19 +101,19 @@ extension HealthCardType { /// /// - Returns: `AutCertificateResponse` after trying to read the authentication certificate file /// and ESignInfo associated to it - public func readAutCertificate() async throws -> AutCertificateResponse { + public func readAutCertificateAsync() async throws -> AutCertificateResponse { CommandLogger.commands.append(Command(message: "Read Auth Certificate", type: .description)) let expectedFcpLength = currentCardChannel.maxResponseLength guard let info = self.status.type?.autCertInfo else { throw HealthCard.Error.unsupportedCardType } - let (status, fcp) = try await selectDedicated(file: info.certificate, fcp: true, length: expectedFcpLength) + let (status, fcp) = try await selectDedicatedAsync(file: info.certificate, fcp: true, length: expectedFcpLength) guard let fcp = fcp, let readSize = fcp.readSize else { throw ReadError.fcpMissingReadSize(state: status) } - let certificate = try await readSelectedFile(expected: Int(readSize)) + let certificate = try await readSelectedFileAsync(expected: Int(readSize)) return AutCertificateResponse(info: info, certificate: certificate) } } @@ -208,7 +208,7 @@ extension HealthCardType { /// - Note: If `data` is already hashed properly and/or needs no hashing, you must provide a no-op hasher /// e.g. { data, _ in return data } /// - Returns: HealthCardResponseType after PsoDSA.sign trying to sign the given data on the card - public func sign( + public func signAsync( data: Data, hasher: @escaping (Data, AutCertInfo) -> Data = { data, cert in cert.signatureHashMethod(data) } ) async throws -> HealthCardResponseType { @@ -217,7 +217,7 @@ extension HealthCardType { throw HealthCard.Error.unsupportedCardType } let selectFileCommand = HealthCardCommand.Select.selectFile(with: info.eSign) - let selectFileResponse = try await selectFileCommand.transmit(to: self) + let selectFileResponse = try await selectFileCommand.transmitAsync(to: self) guard selectFileResponse.responseStatus == .success else { throw HealthCard.Error.operational @@ -227,14 +227,14 @@ extension HealthCardType { dfSpecific: true, algorithm: info.algorithm ) - let selectSigningResponse = try await selectSigningCommand.transmit(to: self) + let selectSigningResponse = try await selectSigningCommand.transmitAsync(to: self) guard selectSigningResponse.responseStatus == .success else { throw HealthCard.Error.operational } let digest = hasher(data, info) let psoDsaSignCommand = try HealthCardCommand.PsoDSA.sign(digest) - let psoDsaSignResponse = try await psoDsaSignCommand.transmit(to: self) + let psoDsaSignResponse = try await psoDsaSignCommand.transmitAsync(to: self) return psoDsaSignResponse } } diff --git a/Sources/HealthCardControl/Authentication/HealthCardType+ResetRetryCounter.swift b/Sources/HealthCardControl/Authentication/HealthCardType+ResetRetryCounter.swift index bd7baab..f0b4acd 100644 --- a/Sources/HealthCardControl/Authentication/HealthCardType+ResetRetryCounter.swift +++ b/Sources/HealthCardControl/Authentication/HealthCardType+ResetRetryCounter.swift @@ -114,7 +114,7 @@ extension HealthCardType { dfSpecific: dfSpecific, puk: puk ) - let response = try await command.transmit(to: self, writeTimeout: writeTimeout, readTimeout: readTimeout) + let response = try await command.transmitAsync(to: self, writeTimeout: writeTimeout, readTimeout: readTimeout) let responseStatus = response.responseStatus if ResponseStatus.wrongSecretWarnings.contains(responseStatus) { return .wrongSecretWarning(retryCount: responseStatus.retryCount) @@ -245,7 +245,7 @@ extension HealthCardType { /// - type: Password reference /// - dfSpecific: is Password reference dfSpecific /// - Returns: Response after trying to reset the password's retry counter while setting a new secret - public func resetRetryCounterAndSetNewPin( + public func resetRetryCounterAndSetNewPinAsync( puk: Format2Pin, newPin: Format2Pin, type: EgkFileSystem.Pin = EgkFileSystem.Pin.mrpinHome, @@ -258,7 +258,7 @@ extension HealthCardType { puk: puk, newPin: newPin ) - let response = try await command.transmit(to: self) + let response = try await command.transmitAsync(to: self) let responseStatus = response.responseStatus if ResponseStatus.wrongSecretWarnings.contains(responseStatus) { return .wrongSecretWarning(retryCount: responseStatus.retryCount) @@ -312,7 +312,7 @@ extension HealthCardType { /// - newPin: The new secret of the password object /// - affectedPassWord: convenience `ResetRetryCounterAffectedPassword` selector /// - Returns: Response after trying to reset the password's retry counter while setting a new secret - public func resetRetryCounterAndSetNewPin( + public func resetRetryCounterAndSetNewPinAsync( puk: String, newPin: String, affectedPassWord: ResetRetryCounterAffectedPassword @@ -326,7 +326,7 @@ extension HealthCardType { type = .mrpinHome dfSpecific = false } - return try await resetRetryCounterAndSetNewPin( + return try await resetRetryCounterAndSetNewPinAsync( puk: parsedPuk, newPin: parsedPin, type: type, diff --git a/Sources/HealthCardControl/Authentication/HealthCardType+VerifyPin.swift b/Sources/HealthCardControl/Authentication/HealthCardType+VerifyPin.swift index c18b312..ebcf8e6 100644 --- a/Sources/HealthCardControl/Authentication/HealthCardType+VerifyPin.swift +++ b/Sources/HealthCardControl/Authentication/HealthCardType+VerifyPin.swift @@ -98,7 +98,7 @@ extension HealthCardType { /// - Returns: Response after trying to verify the given PIN-value information against a EgkFileSystem.Pin `type` /// /// - Note: Only supports eGK Card types - public func verify( + public func verifyAsync( pin: Format2Pin, type: EgkFileSystem.Pin, dfSpecific: Bool = false @@ -106,7 +106,7 @@ extension HealthCardType { CommandLogger.commands.append(Command(message: "Verify PIN", type: .description)) let verifyPasswordParameter = (type.rawValue, dfSpecific, pin) let verifyCommand = HealthCardCommand.Verify.verify(password: verifyPasswordParameter) - let verifyResponse = try await verifyCommand.transmit(to: self) + let verifyResponse = try await verifyCommand.transmitAsync(to: self) let responseStatus = verifyResponse.responseStatus if ResponseStatus.wrongSecretWarnings.contains(responseStatus) { return .wrongSecretWarning(retryCount: responseStatus.retryCount) @@ -155,7 +155,7 @@ extension HealthCardType { /// - pin: holds the Pin information for the password /// - affectedPassword: convenience `VerifyPinAffectedPassword` selector /// - Returns: Response after trying to verify the given PIN-value information against a EgkFileSystem.Pin `type` - public func verify( + public func verifyAsync( pin: String, affectedPassword: VerifyPinAffectedPassword ) async throws -> VerifyPinResponse { @@ -167,6 +167,6 @@ extension HealthCardType { type = .mrpinHome dfSpecific = false } - return try await verify(pin: parsedPIN, type: type, dfSpecific: dfSpecific) + return try await verifyAsync(pin: parsedPIN, type: type, dfSpecific: dfSpecific) } } diff --git a/Sources/HealthCardControl/Operations/CardChannelType+CardAID.swift b/Sources/HealthCardControl/Operations/CardChannelType+CardAID.swift index 46da5a9..cce41ce 100644 --- a/Sources/HealthCardControl/Operations/CardChannelType+CardAID.swift +++ b/Sources/HealthCardControl/Operations/CardChannelType+CardAID.swift @@ -84,7 +84,7 @@ extension CardChannelType { /// - writeTimeout: time in seconds. Default: 30 /// - readTimeout: time in seconds. Default: 30 /// - Returns: CardAID - func determineCardAid( + func determineCardAidAsync( writeTimeout: TimeInterval = 30.0, readTimeout: TimeInterval = 30.0 ) async throws -> CardAid { @@ -96,7 +96,7 @@ extension CardChannelType { } else { let expectedLength = expectedLengthWildcard let selectCommand = try HealthCardCommand.Select.selectRootRequestingFcp(expectedLength: expectedLength) - let selectResponse = try await selectCommand.transmit( + let selectResponse = try await selectCommand.transmitAsync( on: self, writeTimeout: writeTimeout, readTimeout: readTimeout diff --git a/Sources/HealthCardControl/Operations/CardChannelType+CardAccess.swift b/Sources/HealthCardControl/Operations/CardChannelType+CardAccess.swift index fdae571..30d02b0 100644 --- a/Sources/HealthCardControl/Operations/CardChannelType+CardAccess.swift +++ b/Sources/HealthCardControl/Operations/CardChannelType+CardAccess.swift @@ -72,7 +72,7 @@ extension CardChannelType { .eraseToAnyPublisher() } - func readKeyAgreementAlgorithm( + func readKeyAgreementAlgorithmAsync( cardAid: CardAid? = nil, writeTimeout: TimeInterval = 30.0, readTimeout: TimeInterval = 30.0 @@ -83,11 +83,11 @@ extension CardChannelType { if let cardAid = cardAid { determinedCardAid = cardAid } else { - determinedCardAid = try await channel.determineCardAid() + determinedCardAid = try await channel.determineCardAidAsync() } let selectCommand = HealthCardCommand.Select.selectFile(with: determinedCardAid.rawValue) - let selectResponse = try await selectCommand.transmit( + let selectResponse = try await selectCommand.transmitAsync( on: channel, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -100,7 +100,7 @@ extension CardChannelType { with: determinedCardAid.efCardAccess, ne: APDU.expectedLengthWildcardShort ) - let readResponse = try await readCommand.transmit( + let readResponse = try await readCommand.transmitAsync( on: channel, writeTimeout: writeTimeout, readTimeout: readTimeout diff --git a/Sources/HealthCardControl/Operations/CardChannelType+Version.swift b/Sources/HealthCardControl/Operations/CardChannelType+Version.swift index 57d6175..03b242b 100644 --- a/Sources/HealthCardControl/Operations/CardChannelType+Version.swift +++ b/Sources/HealthCardControl/Operations/CardChannelType+Version.swift @@ -105,7 +105,7 @@ extension CardChannelType { /// - writeTimeout: interval in seconds. Default: 30 /// - readTimeout: interval in seconds. Default: 30 /// - Returns: HealthCardPropertyType on successful recognition of the AID and EF.Version2 - public func readCardType( + public func readCardTypeAsync( cardAid: CardAid? = nil, writeTimeout: TimeInterval = 30.0, readTimeout: TimeInterval = 30.0 @@ -116,11 +116,11 @@ extension CardChannelType { if let cardAid = cardAid { determinedCardAid = cardAid } else { - determinedCardAid = try await channel.determineCardAid() + determinedCardAid = try await channel.determineCardAidAsync() } let selectCommand = HealthCardCommand.Select.selectFile(with: determinedCardAid.rawValue) - let selectResponse = try await selectCommand.transmit( + let selectResponse = try await selectCommand.transmitAsync( on: channel, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -134,7 +134,7 @@ extension CardChannelType { with: determinedCardAid.efVersion2Sfi, ne: channel.expectedLengthWildcard ) - let readResponse = try await readCommand.transmit( + let readResponse = try await readCommand.transmitAsync( on: channel, writeTimeout: writeTimeout, readTimeout: readTimeout diff --git a/Sources/HealthCardControl/Operations/HealthCardType+ReadFile.swift b/Sources/HealthCardControl/Operations/HealthCardType+ReadFile.swift index fb59681..bc5151f 100644 --- a/Sources/HealthCardControl/Operations/HealthCardType+ReadFile.swift +++ b/Sources/HealthCardControl/Operations/HealthCardType+ReadFile.swift @@ -115,7 +115,7 @@ extension HealthCardType { /// - Throws: Emits `ReadError` on the Publisher in case of failure. /// /// - Returns: `Data` that was read form the currently selected file - public func readSelectedFile( + public func readSelectedFileAsync( expected size: Int?, failOnEndOfFileWarning: Bool = true, offset: Int = 0 @@ -124,7 +124,7 @@ extension HealthCardType { let expectedResponseLength = size ?? 0x10000 let responseLength = min(maxResponseLength, expectedResponseLength) let readFileCommand = try HealthCardCommand.Read.readFileCommand(ne: responseLength, offset: offset) - let readFileResponse = try await readFileCommand.transmit(to: self) + let readFileResponse = try await readFileCommand.transmitAsync(to: self) guard readFileResponse.responseStatus == .success || (!failOnEndOfFileWarning && readFileResponse.responseStatus == .endOfFileWarning) else { @@ -142,7 +142,7 @@ extension HealthCardType { if continueReading { // Continue reading - let continued = try await readSelectedFile( + let continued = try await readSelectedFileAsync( expected: size != nil ? (expectedResponseLength - responseData.count) : nil, failOnEndOfFileWarning: failOnEndOfFileWarning, offset: offset + responseData.count @@ -221,13 +221,13 @@ extension HealthCardType { /// - Throws: `SelectError` (or `ReadError` is case no FCP data could be read and fcp = true) /// /// - Returns: `(ResponseStatus, FileControlParameter?)` after trying to select the given file - public func selectDedicated( + public func selectDedicatedAsync( file: DedicatedFile, fcp: Bool = false, length: Int = 256 ) async throws -> (ResponseStatus, FileControlParameter?) { let selectFileCommand = HealthCardCommand.Select.selectFile(with: file.aid) - let selectFileResponse = try await selectFileCommand.transmit(to: self) + let selectFileResponse = try await selectFileCommand.transmitAsync(to: self) guard selectFileResponse.responseStatus == .success else { @@ -241,7 +241,7 @@ extension HealthCardType { let selectEfCommand = fcp ? try HealthCardCommand.Select.selectEfRequestingFcp(with: fid, expectedLength: length) : HealthCardCommand.Select.selectEf(with: fid) - let selectEfResponse = try await selectEfCommand.transmit(to: self) + let selectEfResponse = try await selectEfCommand.transmitAsync(to: self) guard selectEfResponse.responseStatus == .success else { diff --git a/Sources/HealthCardControl/SecureMessaging/CardType+SecureMessaging.swift b/Sources/HealthCardControl/SecureMessaging/CardType+SecureMessaging.swift index 3295690..ee60a9b 100644 --- a/Sources/HealthCardControl/SecureMessaging/CardType+SecureMessaging.swift +++ b/Sources/HealthCardControl/SecureMessaging/CardType+SecureMessaging.swift @@ -86,26 +86,26 @@ extension CardType { /// - writeTimeout: time in seconds. Default: 30 /// - readTimeout: time in seconds. Default 30 /// - Returns: SecureHealthCard - public func openSecureSession( + public func openSecureSessionAsync( can: CAN, writeTimeout: TimeInterval = 30, readTimeout: TimeInterval = 30 - ) async throws -> HealthCardType { + ) async throws -> SecureHealthCardType { CommandLogger.commands.append(Command(message: "Open secure Session", type: .description)) let channel = try openBasicChannel() // Read/Determine ApplicationIdentifier of the card's initial application - let cardAid = try await channel.determineCardAid(writeTimeout: writeTimeout, readTimeout: readTimeout) + let cardAid = try await channel.determineCardAidAsync(writeTimeout: writeTimeout, readTimeout: readTimeout) // Read EF.CardAccess and determine the algorithm for the key agreement (e.g. PACE) - let keyAgreementAlgorithm = try await channel.readKeyAgreementAlgorithm(cardAid: cardAid) + let keyAgreementAlgorithm = try await channel.readKeyAgreementAlgorithmAsync(cardAid: cardAid) // Read EF.Version2 and determine HealthCardPropertyType - let cardType = try await channel.readCardType( + let cardType = try await channel.readCardTypeAsync( cardAid: cardAid, writeTimeout: writeTimeout, readTimeout: readTimeout ) let healthCard = try HealthCard(card: self, status: .valid(cardType: cardType)) - let sessionKey = try await keyAgreementAlgorithm.negotiateSessionKey( + let sessionKey = try await keyAgreementAlgorithm.negotiateSessionKeyAsync( card: healthCard, can: can, writeTimeout: writeTimeout, @@ -125,6 +125,7 @@ extension CardType { /// - writeTimeout: time in seconds. Default: 30 /// - readTimeout: time in seconds. Default 30 /// - Returns: Publisher that negotiates a secure session when scheduled to run. + @available(*, deprecated, message: "Use structured concurrency version instead") public func openSecureSession(can: String, writeTimeout: TimeInterval = 30, readTimeout: TimeInterval = 30) -> AnyPublisher { let parsedCan: CAN @@ -135,4 +136,28 @@ extension CardType { } return openSecureSession(can: parsedCan, writeTimeout: writeTimeout, readTimeout: readTimeout) } + + /// Open a secure session with a Card for further scheduling/attaching Publisher commands + /// + /// - Note: The healthCard provided by the Combine operation chain should be used for the commands + /// to be executed on the secure channel. + /// After the chain has completed the session should be invalidated/closed. + /// + /// - Parameters: + /// - can: The Channel access number for the session + /// - writeTimeout: time in seconds. Default: 30 + /// - readTimeout: time in seconds. Default 30 + /// - Returns: SecureHealthCard + public func openSecureSessionAsync( + can: String, + writeTimeout: TimeInterval = 30, + readTimeout: TimeInterval = 30 + ) async throws -> SecureHealthCardType { + let parsedCan = try CAN.from(Data(can.utf8)) + return try await openSecureSessionAsync( + can: parsedCan, + writeTimeout: writeTimeout, + readTimeout: readTimeout + ) + } } diff --git a/Sources/HealthCardControl/SecureMessaging/KeyAgreement.swift b/Sources/HealthCardControl/SecureMessaging/KeyAgreement.swift index 5171766..acb8f42 100644 --- a/Sources/HealthCardControl/SecureMessaging/KeyAgreement.swift +++ b/Sources/HealthCardControl/SecureMessaging/KeyAgreement.swift @@ -153,7 +153,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length /// - readTimeout: timeout in seconds. time <= 0 is no timeout /// - Returns: Instance of `SecureMessaging` employing the PACE key /// that both this application and the card agreed on. - public func negotiateSessionKey( + public func negotiateSessionKeyAsync( card: HealthCardType, can: CAN, writeTimeout: TimeInterval = 10, @@ -162,13 +162,13 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length switch self { case .idPaceEcdhGmAesCbcCmac128: // Set security environment - _ = try await step0PaceEcdhGmAesCbcCmac128( + _ = try await step0PaceEcdhGmAesCbcCmac128Async( card: card, writeTimeout: writeTimeout, readTimeout: readTimeout ) // Request nonceZ from card and decrypt it to nonceS as Data - let nonceS = try await step1PaceEcdhGmAesCbcCmac128( + let nonceS = try await step1PaceEcdhGmAesCbcCmac128Async( card: card, can: can, writeTimeout: writeTimeout, @@ -176,7 +176,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length ) // Generate first own public key (PK1_PCD) and send it to card. // Receive first public key (PK1_PICC) from card - let (pk2Pcd, keyPair2) = try await step2PaceEcdhGmAesCbcCmac128( + let (pk2Pcd, keyPair2) = try await step2PaceEcdhGmAesCbcCmac128Async( card: card, nonceS: nonceS, writeTimeout: writeTimeout, @@ -184,7 +184,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length ) // Send own public key PK2_PCD to card and receive second public key (PK2_PICC) from card. // Derive PaceKey from all the information. - let (pk2Picc, paceKey) = try await step3PaceEcdhGmAesCbcCmac128( + let (pk2Picc, paceKey) = try await step3PaceEcdhGmAesCbcCmac128Async( card: card, pk2Pcd: pk2Pcd, keyPair2: keyPair2, @@ -194,7 +194,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length // Derive MAC_PCD from a key mac and from a auth token and send it to card // so the card can verify it. // Receive MAC_PICC from card and verify it. - let verifyMacPicc = try await step4PaceEcdhGmAesCbcCmac128( + let verifyMacPicc = try await step4PaceEcdhGmAesCbcCmac128Async( card: card, pk2Picc: pk2Picc, pk2Pcd: pk2Pcd, @@ -234,7 +234,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length } /// Set the appropriate security environment on card. - private static func step0PaceEcdhGmAesCbcCmac128( + private static func step0PaceEcdhGmAesCbcCmac128Async( card: HealthCardType, writeTimeout: TimeInterval, readTimeout: TimeInterval @@ -248,7 +248,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length dfSpecific: false, oid: oid ) - let selectPaceResponse = try await selectPaceCommand.transmit( + let selectPaceResponse = try await selectPaceCommand.transmitAsync( to: card, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -283,14 +283,14 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length } /// Request nonceZ from card and decrypt it to nonceS as Data - private static func step1PaceEcdhGmAesCbcCmac128( + private static func step1PaceEcdhGmAesCbcCmac128Async( card: HealthCardType, can: CAN, writeTimeout: TimeInterval, readTimeout: TimeInterval ) async throws -> Data { let paceStep1aCommand = HealthCardCommand.PACE.step1a() - let paceStep1aResponse = try await paceStep1aCommand.transmit( + let paceStep1aResponse = try await paceStep1aCommand.transmitAsync( to: card, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -348,7 +348,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length /// Receive first public key (PK1_PICC) from card /// Calculate a shared secret generating point gTilde /// Generate a second keyPair2 PK2_PICD and public key PK2_PCD = gTilde * keyPair2.privateKey - private static func step2PaceEcdhGmAesCbcCmac128( + private static func step2PaceEcdhGmAesCbcCmac128Async( card: HealthCardType, nonceS: Data, writeTimeout: TimeInterval, @@ -358,7 +358,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length let paceStep2aCommand = try HealthCardCommand.PACE.step2a(publicKey: keyPair1.publicKey.x962Value()) - let pk1PiccResponse = try await paceStep2aCommand.transmit( + let pk1PiccResponse = try await paceStep2aCommand.transmitAsync( to: card, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -404,7 +404,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length /// Send own public key PK2_PCD to card and receive second public key (PK2_PICC) from card /// Derive PACE key from all the information - private static func step3PaceEcdhGmAesCbcCmac128( + private static func step3PaceEcdhGmAesCbcCmac128Async( card: HealthCardType, pk2Pcd: BrainpoolP256r1.KeyExchange.PublicKey, keyPair2: BrainpoolP256r1.KeyExchange.PrivateKey, @@ -412,7 +412,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length readTimeout: TimeInterval ) async throws -> (BrainpoolP256r1.KeyExchange.PublicKey, AES128PaceKey) { let paceStep3Command = try HealthCardCommand.PACE.step3a(publicKey: pk2Pcd.x962Value()) - let pk2PiccResponse = try await paceStep3Command.transmit( + let pk2PiccResponse = try await paceStep3Command.transmitAsync( to: card, writeTimeout: writeTimeout, readTimeout: readTimeout @@ -468,7 +468,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length /// Derive MAC_PCD from a key mac and from a auth token and send it to card for verification /// Receive MAC_PICC from card and verify it - private static func step4PaceEcdhGmAesCbcCmac128( // swiftlint:disable:this function_parameter_count + private static func step4PaceEcdhGmAesCbcCmac128Async( // swiftlint:disable:this function_parameter_count card: HealthCardType, pk2Picc: BrainpoolP256r1.KeyExchange.PublicKey, pk2Pcd: BrainpoolP256r1.KeyExchange.PublicKey, @@ -484,7 +484,7 @@ public enum KeyAgreement { // swiftlint:disable:this type_body_length ) let macPcdToken = macPcd.prefix(algorithm.macTokenPrefixSize) let paceStep4aCommand = try HealthCardCommand.PACE.step4a(token: macPcdToken) - let macPiccResponse = try await paceStep4aCommand.transmit( + let macPiccResponse = try await paceStep4aCommand.transmitAsync( to: card, writeTimeout: writeTimeout, readTimeout: readTimeout diff --git a/Sources/HealthCardControl/SecureMessaging/SecureCardChannel.swift b/Sources/HealthCardControl/SecureMessaging/SecureCardChannel.swift index e62ce87..a04613b 100644 --- a/Sources/HealthCardControl/SecureMessaging/SecureCardChannel.swift +++ b/Sources/HealthCardControl/SecureMessaging/SecureCardChannel.swift @@ -49,6 +49,7 @@ internal class SecureCardChannel: CardChannelType { channel = card.currentCardChannel } + @_disfavoredOverload func transmit(command: CommandType, writeTimeout: TimeInterval, readTimeout: TimeInterval) throws -> ResponseType { DLog(">> \(command.bytes.hexString())") // we only log the header bytes to prevent logging user's PIN diff --git a/Sources/NFCCardReaderProvider/Card/NFCCardChannel.swift b/Sources/NFCCardReaderProvider/Card/NFCCardChannel.swift index cdf7bff..7fa3d21 100644 --- a/Sources/NFCCardReaderProvider/Card/NFCCardChannel.swift +++ b/Sources/NFCCardReaderProvider/Card/NFCCardChannel.swift @@ -107,7 +107,6 @@ class NFCCardChannel: CardChannelType { } } - // todo-timeout: implement timeout? func transmitAsync( command: CommandType, writeTimeout _: TimeInterval, @@ -140,18 +139,14 @@ class NFCCardChannel: CardChannelType { do { (data, sw1, sw2) = try await tag.sendCommand(apdu: apdu) } catch { - throw NFCCardError.nfcTag(error: error.asCoreNFCError()) + throw error.asCoreNFCError() } let response = "[\(Data(data + [sw1, sw2]).hexString())]" DLog("RESPONSE: \(response)") CommandLogger.commands.append(Command(message: response, type: .response)) - do { - return try APDU.Response(body: data, sw1: sw1, sw2: sw2) - } catch { - throw CardError.connectionError(error) - } + return try APDU.Response(body: data, sw1: sw1, sw2: sw2) } func close() throws { diff --git a/Sources/NFCCardReaderProvider/NFCHealthCardSession.swift b/Sources/NFCCardReaderProvider/NFCHealthCardSession.swift new file mode 100644 index 0000000..d40eae9 --- /dev/null +++ b/Sources/NFCCardReaderProvider/NFCHealthCardSession.swift @@ -0,0 +1,399 @@ +// +// Copyright (c) 2024 gematik GmbH +// +// Licensed under the Apache License, Version 2.0 (the License); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an 'AS IS' BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if os(iOS) + +import CardReaderProviderApi +import Combine +import CoreNFC +import Foundation +import GemCommonsKit +import HealthCardAccess +import HealthCardControl + +/// `NFCHealthCardSession` facilitates communication between iOS applications and NFC-enabled health cards. +/// It leverages Core NFC to establish a session with a health card and perform operations on it, +/// such as reading data or executing commands, +/// in a secure manner through a previously established secure channel (PACE). +/// +/// Initialization +/// +/// To create an instance of NFCHealthCardSession, the following parameters are required: +/// +/// - pollingOption: Specifies the NFC polling option to use. +/// The default is .iso14443, which is suitable for most health cards. +/// - queue: The dispatch queue on which the NFC session callbacks are executed. +/// By default, it uses .global(qos: .userInitiated). +/// - messages: A struct of type Messages containing various user-facing messages displayed during NFC operations. +/// - can: The Card Access Number (CAN) required to establish a secure channel with the health card. +/// - operation: A closure that takes a `NFCHealthCardSessionHandle` as its argument +/// and allows for asynchronous execution of NFC operations. +/// +/// `NFCHealthCardSessionHandle` provides an abstraction to the `NFCTagReaderSession` +/// allowing the updating of the user interface message and the invalidation of the session. +/// It also gives access to the `card`, representing the health card with which a secure channel has been established. +/// +/// ```swift +/// let nfcSession = NFCHealthCardSession( +/// messages: Messages( +/// discoveryMessage: "Hold your iPhone near the health card", +/// connectMessage: "Initializing...", +/// secureChannelMessage: "Establishing secure connection...", +/// noCardMessage: "No card detected. Please try again.", +/// multipleCardsMessage: "Multiple cards detected. Please present only one card.", +/// unsupportedCardMessage: "Unsupported card. Please use a valid health card.", +/// connectionErrorMessage: "An error occurred during connection. Please try again." +/// ), +/// can: "123456", +/// operation: { sessionHandle in +/// // Perform operations with sessionHandle +/// // A secure channel (PACE) is established initially before executing the handle's operations +/// // Return the result of the operation +/// sessionHandle.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_withNewPin", comment: "")) +/// let changeReferenceDataResponse = try await sessionHandle.card.changeReferenceDataSetNewPin( +/// old: format2OldPin, +/// new: format2NewPin, +/// type: EgkFileSystem.Pin.mrpinHome, +/// dfSpecific: false +/// ) +/// if case ChangeReferenceDataResponse.success = changeReferenceDataResponse { +/// sessionHandle.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) +/// return changeReferenceDataResponse +/// } else { +/// // handle this +/// } +/// ) +/// ``` +/// Methods +/// +/// - executeOperation(): Asynchronously executes the operation provided during initialization. +/// This method establishes a secure channel (PACE) with the health card before executing the operation. +/// It returns the result of the operation or throws an error if the session could not be initialized +/// or the operation fails. This method should be called only once. The thrown error type is NFCHealthSessionError. +/// +/// ```swift +/// let signedData: Data +/// do { +/// signedData = try await nfcHealthCardSession.executeOperation() +/// +/// Task { @MainActor in self.pState = .value(signedData) } +/// } catch NFCHealthCardSessionError.coreNFC(.userCanceled) { +/// // error type is always `NFCHealthCardSessionError` +/// // here we especially handle when the user canceled the session +/// Task { @MainActor in self.pState = .idle } // Do some view-property update +/// // Calling .invalidateSession() is not strictly necessary +/// // since nfcHealthCardSession does it while it's de-initializing. +/// nfcHealthCardSession.invalidateSession(with: nil) +/// return +/// } catch { +/// Task { @MainActor in self.pState = .error(error) } +/// nfcHealthCardSession.invalidateSession(with: error.localizedDescription) +/// return +/// } +/// ``` +/// +/// - invalidateSession(with error: String?): Invalidates the current NFC session. +/// If an error message is provided, the session ends with that error message; otherwise, it ends normally. + +public class NFCHealthCardSession: NSObject, NFCTagReaderSessionDelegate { + private typealias OperationCheckedContinuation = CheckedContinuation + private var operationContinuation: OperationCheckedContinuation? + + private let messages: Messages + private let can: String + + private var session: NFCTagReaderSession? + + var operation: (NFCHealthCardSessionHandle) async throws -> Output + + /// Session object that has a handle to a NFC HealthCard to execute further commands on. + /// A secure channel (PACE) is established initially before executing the handle's operations. + /// + /// The initializer only returns nil if `NFCTagReaderSession` could not be initialized. + /// + /// - Parameters: + /// - pollingOption: default iso14443 + /// - queue: default .global(qos: .userInitiated) + /// - messages: the NFC alert dialog messages for the various states + /// - can: the card access number necessary to establish the secure channel + /// - operation: closure with a `NFCHealthCardSessionHandle` to send/receive commands/responses + /// to/from the NFC HealthCard and to update the user's interface message + public init?( + pollingOption: NFCTagReaderSession.PollingOption = .iso14443, + on queue: DispatchQueue = .global(qos: .userInitiated), + messages: Messages, + can: String, + operation: @escaping ((NFCHealthCardSessionHandle) async throws -> Output) + ) { + self.messages = messages + self.can = can + self.operation = operation + super.init() + + guard let mNFCReaderSession = NFCTagReaderSession( + pollingOption: pollingOption, + delegate: self, + queue: queue + ) + else { + DLog("Could not start discovery for NFCCardReader: refused to init a NFCTagReaderSession") + return nil + } + + session = mNFCReaderSession + } + + /// Executes the operation on the NFC HealthCard. + /// A secure channel (PACE) is established before executing the operation. + /// It returns the result of the operation or throws an error if the session could not be initialized + /// or the operation fails. + /// - Returns: The result of the operation. + /// - Throws: `NFCHealthCardSessionError` + /// + /// - Note: NFCHealthCardSessionError members of special interest are: + /// NFCHealthCardSessionError.coreNFC(.userCanceled) and NFCHealthCardSessionError.wrongCAN + public func executeOperation() async throws -> Output { + guard let session = self.session + else { + throw NFCHealthCardSessionError.couldNotInitializeSession + } + session.alertMessage = messages.discoveryMessage + DLog("Starting session: \(String(describing: self.session))") + session.begin() + + let outcome = try await withCheckedThrowingContinuation { continuation in + self.operationContinuation = continuation + } + return outcome + } + + deinit { + DLog("Deinit MyNFCSession") + session?.invalidate() + } + + /// Invalidates the current NFC session. Optionally, an error message can be provided + /// to end the session with a specific error. + /// - Parameter error: An optional error message. If provided, the session ends with this error message; + /// otherwise, it ends normally. + public func invalidateSession(with error: String?) { + if let error = error { + session?.invalidate(errorMessage: error) + } else { + session?.invalidate() + } + } + + // MARK: - NFCTagReaderSessionDelegate + + public func tagReaderSessionDidBecomeActive(_: NFCTagReaderSession) { + DLog("NFC reader session became active") + } + + public func tagReaderSession(_: NFCTagReaderSession, didInvalidateWithError error: Swift.Error) { + DLog("NFC reader session was invalidated: \(error)") + let coreNFCError = error.asCoreNFCError() + operationContinuation?.resume(throwing: NFCHealthCardSessionError.coreNFC(coreNFCError)) + operationContinuation = nil + } + + // swiftlint:disable:next function_body_length + public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + DLog("tagReaderSession:didDetect - [\(tags)]") + if tags.count > 1 { + session.alertMessage = messages.multipleCardsMessage + DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) { + session.restartPolling() + } + return + } + + guard let tag = tags.first else { + session.alertMessage = messages.noCardMessage + DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(500)) { + session.restartPolling() + } + return + } + guard case let .iso7816(iso7816NfcTag) = tag else { + session.invalidate(errorMessage: messages.unsupportedCardMessage) + operationContinuation?.resume(throwing: NFCHealthCardSessionError.unsupportedTag) + operationContinuation = nil + return + } + + session.alertMessage = messages.connectMessage + + Task { + do { + try await session.connect(to: tag) + } catch { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.coreNFC(error.asCoreNFCError())) + operationContinuation = nil + return + } + + session.alertMessage = messages.secureChannelMessage + let card = NFCCard(isoTag: iso7816NfcTag) + + let secureHealthCard: HealthCardType + do { + secureHealthCard = try await card.openSecureSessionAsync(can: can) + } catch let error as CoreNFCError { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.coreNFC(error)) + operationContinuation = nil + return + } catch HealthCardControl.KeyAgreement.Error.macPcdVerificationFailedOnCard { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.wrongCAN) + operationContinuation = nil + return + } catch { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.establishingSecureChannel(error)) + operationContinuation = nil + return + } + + let myNFCCardSession = DefaultNFCHealthCardSessionHandle( + card: secureHealthCard, + session: session + ) + + do { + let outcome = try await operation(myNFCCardSession) + operationContinuation?.resume(returning: outcome) + } catch let error as CoreNFCError { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.coreNFC(error)) + operationContinuation = nil + return + } catch { + operationContinuation?.resume(throwing: NFCHealthCardSessionError.operation(error)) + operationContinuation = nil + return + } + } + } +} + +/// The (only) error type that is thrown by `.executeOperation(). +public enum NFCHealthCardSessionError: Swift.Error { + /// Indicates that the NFC session could not be initialized. + case couldNotInitializeSession + + /// Represents an error when the detected tag is not supported, e.g. that not a Health Card. + case unsupportedTag + + /// Encapsulates errors originating from the CoreNFC framework. This includes, but is not limited to, + /// communication errors, user cancellation, or configuration issues. + /// `CoreNFCError` is a bridge from `NFCReaderError`. + case coreNFC(CoreNFCError) + + /// Signifies that the provided CAN (Card Access Number) is incorrect or failed verification, preventing + /// establishment of a secure channel. It's a common sub case of the `establishingSecureChannel` error. + case wrongCAN + + /// Occurs when establishing a secure channel with the health card fails. This includes errors during key agreement, + /// authentication, or other security protocol failures. + case establishingSecureChannel(Swift.Error) + + /// Generic error for failures during operation execution. This can include APDU de-/serialization errors, and + /// errors thrown by the operation's instructions itself. + case operation(Swift.Error) +} + +/// Abstraction to the NFCTagReaderSession to update the alertMessage that is being displayed to the user. +/// And to close/invalidate the session +public protocol NFCHealthCardSessionHandle { + /// Update the NFC Dialog message + func updateAlert(message: String) + + /// End session + /// + /// - Parameter error: when set the session will end erroneously + func invalidateSession(with error: String?) + + /// The underlying Card for the emitted NFCCardSession + /// The secure card channel has already been established initially + var card: HealthCardType { get } +} + +private struct DefaultNFCHealthCardSessionHandle: NFCHealthCardSessionHandle { + let card: HealthCardType + let session: NFCTagReaderSession + + func updateAlert(message: String) { + Task { @MainActor in self.session.alertMessage = message } + } + + func invalidateSession(with error: String?) { + Task { @MainActor in + if let error = error { + session.invalidate(errorMessage: error) + } else { + session.invalidate() + } + } + } +} + +extension NFCHealthCardSession { + /// NFCTagReaderSession messages + public struct Messages { + /// The message that is being displayed when polling for a NFC Card + public let discoveryMessage: String + /// The message when the card is being initialized for downstream usage + public let connectMessage: String + /// The message during establishing a secure card channel after the connect + public let secureChannelMessage: String + /// The message when 'something else' as a card is found, but not a card + public let noCardMessage: String + /// The message to display when multiple NFC Cards were detected + public let multipleCardsMessage: String + /// The message when the card type is unsupported + public let unsupportedCardMessage: String + /// The generic error message + public let connectionErrorMessage: String + + /// Messages constructor + /// + /// - Parameters: + /// - discoveryMessage: The message that is being displayed when polling for a NFC Card + /// - connectMessage: The message when the card is being initialized for downstream usage + /// - secureChannelMessage: The message during establishing a secure card channel after the connect + /// - noCardMessage: The message when 'something else' as a card is found, but not a card + /// - multipleCardsMessage: The message to display when multiple NFC Cards were detected + /// - unsupportedCardMessage: The message when the card type is unsupported + /// - connectionErrorMessage: The generic (communication) error message + public init( + discoveryMessage: String, + connectMessage: String, + secureChannelMessage: String, + noCardMessage: String, + multipleCardsMessage: String, + unsupportedCardMessage: String, + connectionErrorMessage: String + ) { + self.discoveryMessage = discoveryMessage + self.connectMessage = connectMessage + self.secureChannelMessage = secureChannelMessage + self.noCardMessage = noCardMessage + self.multipleCardsMessage = multipleCardsMessage + self.unsupportedCardMessage = unsupportedCardMessage + self.connectionErrorMessage = connectionErrorMessage + } + } +} + +#endif diff --git a/Sources/NFCCardReaderProvider/Reader/NFCTagReaderSession+Publisher.swift b/Sources/NFCCardReaderProvider/Reader/NFCTagReaderSession+Publisher.swift index b95fd84..8124d0c 100644 --- a/Sources/NFCCardReaderProvider/Reader/NFCTagReaderSession+Publisher.swift +++ b/Sources/NFCCardReaderProvider/Reader/NFCTagReaderSession+Publisher.swift @@ -111,6 +111,7 @@ extension NFCTagReaderSession { /// - queue: default .global(qos: .userInitiated) /// - messages: the NFC alert dialog messages for the various states /// - Returns: NFCTagReaderSession.Publisher + @available(*, deprecated, message: "Use NFCHealthCardSession instead") public static func publisher(for pollingOption: PollingOption = .iso14443, on queue: DispatchQueue = .global(qos: .userInitiated), messages: Messages) -> Publisher { diff --git a/Sources/NFCDemo/CallOnMainThread.swift b/Sources/NFCDemo/CallOnMainThread.swift deleted file mode 100644 index 1eaa3ce..0000000 --- a/Sources/NFCDemo/CallOnMainThread.swift +++ /dev/null @@ -1,32 +0,0 @@ -// swiftlint:disable:this file_name -// -// Copyright (c) 2023 gematik GmbH -// -// Licensed under the Apache License, Version 2.0 (the License); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an 'AS IS' BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -/// main thread callback closure -public typealias Callback = () -> Void - -/** - Helper function to ensure that the callback is executed on the main thread. - */ -public func callOnMainThread(_ callback: @escaping Callback) { - if Thread.isMainThread { - callback() - } else { - DispatchQueue.main.sync(execute: callback) - } -} diff --git a/Sources/NFCDemo/NFC/HealthCardControl+LocalizedError.swift b/Sources/NFCDemo/NFC/HealthCardControl+LocalizedError.swift index 4b45753..45ea18a 100644 --- a/Sources/NFCDemo/NFC/HealthCardControl+LocalizedError.swift +++ b/Sources/NFCDemo/NFC/HealthCardControl+LocalizedError.swift @@ -22,7 +22,7 @@ extension KeyAgreement.Error: LocalizedError { public var errorDescription: String? { switch self { case .illegalArgument: - return "illegalAgument" + return "illegalArgument" case .unexpectedFormedAnswerFromCard: return "unexpectedFormedAnswerFromCard" case .resultOfEcArithmeticWasInfinite: @@ -36,7 +36,7 @@ extension KeyAgreement.Error: LocalizedError { case .efCardAccessNotAvailable: return "efCardAccessNotAvailable" case let .unsupportedKeyAgreementAlgorithm(identifier): - return "unsupportedKeyAgreementAlgorithm with identifeir: \(identifier)" + return "unsupportedKeyAgreementAlgorithm with identifier: \(identifier)" @unknown default: return "unknown KeyAgreement error" } diff --git a/Sources/NFCDemo/NFC/NFCCardReaderProvider+LocalizedError.swift b/Sources/NFCDemo/NFC/NFCCardReaderProvider+LocalizedError.swift index c3eb2d6..b670553 100644 --- a/Sources/NFCDemo/NFC/NFCCardReaderProvider+LocalizedError.swift +++ b/Sources/NFCDemo/NFC/NFCCardReaderProvider+LocalizedError.swift @@ -18,13 +18,54 @@ import CoreNFC import NFCCardReaderProvider -extension NFCTagReaderSession.Error: LocalizedError { +extension NFCHealthCardSessionError: LocalizedError { public var errorDescription: String? { switch self { case .couldNotInitializeSession: return "NFCTagReaderSession could not be initalized" case .unsupportedTag: return "NFCTagReaderSession.Error: The read tag is not supported" + case let .coreNFC(error: error): + switch error { + case let .tagConnectionLost(nFCReaderError): + return nFCReaderError.localizedDescription + case let .sessionTimeout(nFCReaderError): + return nFCReaderError.localizedDescription + + case let .sessionInvalidated(nFCReaderError): + return nFCReaderError.localizedDescription + + case let .userCanceled(nFCReaderError): + return nFCReaderError.localizedDescription + + case let .unsupportedFeature(nFCReaderError): + return nFCReaderError.localizedDescription + + case let .other(nFCReaderError): + return nFCReaderError.localizedDescription + + case let .unknown(error): + return error.localizedDescription + } + case .wrongCAN: + return "Wrong CAN (macPcdVerificationFailedOnCard)!" + case let .establishingSecureChannel(error): + return error.localizedDescription + case let .operation(error): + return error.localizedDescription + @unknown default: + return "unknown NFCHealthCardSessionError" + } + } +} + +extension NFCTagReaderSession.Error: LocalizedError { + public var errorDescription: String? { + switch self { + case .couldNotInitializeSession: + return "NFCTagReaderSession could not be initialized" + case .unsupportedTag: + return "NFCTagReaderSession.Error: The read tag is not supported" case let .nfcTag(error: error): switch error { case let .tagConnectionLost(nFCReaderError): diff --git a/Sources/NFCDemo/NFC/NFCChangeReferenceDataController.swift b/Sources/NFCDemo/NFC/NFCChangeReferenceDataController.swift index 2ed374d..d930c91 100644 --- a/Sources/NFCDemo/NFC/NFCChangeReferenceDataController.swift +++ b/Sources/NFCDemo/NFC/NFCChangeReferenceDataController.swift @@ -25,8 +25,8 @@ import NFCCardReaderProvider public class NFCChangeReferenceDataController: ChangeReferenceData { public enum Error: Swift.Error, LocalizedError { - /// In case the PIN, PUK or CAN could not be constructed from input case cardError(NFCTagReaderSession.Error) + /// In case the PIN, PUK or CAN could not be constructed from input case invalidCanOrPinFormat case wrongPin(retryCount: Int) case commandBlocked @@ -48,6 +48,7 @@ public class NFCChangeReferenceDataController: ChangeReferenceData { } } + @MainActor @Published private var pState: ViewState = .idle var state: Published>.Publisher { @@ -56,17 +57,17 @@ public class NFCChangeReferenceDataController: ChangeReferenceData { var cancellable: AnyCancellable? - func dismissError() { + @MainActor + func dismissError() async { if pState.error != nil { - callOnMainThread { - self.pState = .idle - } + pState = .idle } } - let messages = NFCTagReaderSession.Messages( + let messages = NFCHealthCardSession.Messages( discoveryMessage: NSLocalizedString("nfc_txt_discoveryMessage", comment: ""), connectMessage: NSLocalizedString("nfc_txt_connectMessage", comment: ""), + secureChannelMessage: NSLocalizedString("nfc_txt_secureChannel", comment: ""), noCardMessage: NSLocalizedString("nfc_txt_noCardMessage", comment: ""), multipleCardsMessage: NSLocalizedString("nfc_txt_multipleCardsMessage", comment: ""), unsupportedCardMessage: NSLocalizedString("nfc_txt_unsupportedCardMessage", comment: ""), @@ -74,116 +75,63 @@ public class NFCChangeReferenceDataController: ChangeReferenceData { ) // swiftlint:disable:next function_body_length - func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) { - if case .loading = pState { return } - callOnMainThread { + func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) async { + if case .loading = await pState { return } + Task { @MainActor in self.pState = .loading(nil) } - let canData: CAN let format2OldPin: Format2Pin let format2NewPin: Format2Pin do { - canData = try CAN.from(Data(can.utf8)) format2OldPin = try Format2Pin(pincode: oldPin) format2NewPin = try Format2Pin(pincode: newPin) } catch { - callOnMainThread { + Task { @MainActor in self.pState = .error(Error.invalidCanOrPinFormat) } return } - cancellable = NFCTagReaderSession.publisher(messages: messages) - .mapError { Error.cardError($0) as Swift.Error } - .flatMap { (session: NFCCardSession) -> AnyPublisher, Swift.Error> in - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_secure_channel", comment: "")) - return session.card // swiftlint:disable:this trailing_closure - .openSecureSession(can: canData, writeTimeout: 0, readTimeout: 0) - .userMessage( - session: session, - message: NSLocalizedString("nfc_txt_msg_reset_withNewPin", comment: "") - ) - .changeReferenceDataSetNewPin( - oldPin: format2OldPin, - newPin: format2NewPin, - type: EgkFileSystem.Pin.mrpinHome, - dfSpecific: false - ) - - .map { _ in true } - .map(ViewState.value) - .handleEvents(receiveOutput: { state in - if let value = state.value, value == true { - session - .updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", - comment: "")) - session.invalidateSession(with: nil) - } else { - session.invalidateSession( - with: state.error?.localizedDescription ?? NSLocalizedString( - "nfc_txt_msg_failure", - comment: "" - ) - ) - } - }) - .mapError { error in - session.invalidateSession(with: error.localizedDescription) - return error - } - .eraseToAnyPublisher() - } - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - if case let .failure(error) = completion { - if case let .cardError(readerError) = error as? NFCChangeReferenceDataController.Error, - case let .nfcTag(error: tagError) = readerError, - case .userCanceled = tagError { - self?.pState = .idle - } else { - self?.pState = .error(error) - } - } else { - self?.pState = .idle - } - self?.cancellable?.cancel() - }, - receiveValue: { [weak self] value in - self?.pState = value - } - ) - } -} - -extension Publisher where Output == HealthCardType, Self.Failure == Swift.Error { - func changeReferenceDataSetNewPin( - oldPin: Format2Pin, - newPin: Format2Pin, - type: EgkFileSystem.Pin, - dfSpecific: Bool - ) -> AnyPublisher { - flatMap { secureCard in - secureCard.changeReferenceDataSetNewPin( - old: oldPin, - new: newPin, - type: type, - dfSpecific: dfSpecific + guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_withNewPin", comment: "")) + let changeReferenceDataResponse = try await session.card.changeReferenceDataSetNewPin( + old: format2OldPin, + new: format2NewPin, + type: EgkFileSystem.Pin.mrpinHome, + dfSpecific: false ) - .tryMap { response in - if case ChangeReferenceDataResponse.success = response { - return secureCard - } - if case let ChangeReferenceDataResponse.wrongSecretWarning(retryCount: count) = response { + if case ChangeReferenceDataResponse.success = changeReferenceDataResponse { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) + return changeReferenceDataResponse + } else { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_failure", comment: "")) + if case let ChangeReferenceDataResponse + .wrongSecretWarning(retryCount: count) = changeReferenceDataResponse { throw NFCChangeReferenceDataController.Error.wrongPin(retryCount: count) } - if case ChangeReferenceDataResponse.commandBlocked = response { + if case ChangeReferenceDataResponse.commandBlocked = changeReferenceDataResponse { throw NFCChangeReferenceDataController.Error.commandBlocked } // else throw NFCChangeReferenceDataController.Error.otherError } + }) + else { + Task { @MainActor in self.pState = .error(NFCTagReaderSession.Error.couldNotInitializeSession) } + return + } + + do { + _ = try await nfcHealthCardSession.executeOperation() + Task { @MainActor in self.pState = .value(true) } + } catch NFCHealthCardSessionError.coreNFC(.userCanceled) { + nfcHealthCardSession.invalidateSession(with: nil) + Task { @MainActor in self.pState = .idle } + return + } catch { + nfcHealthCardSession.invalidateSession(with: error.localizedDescription) + Task { @MainActor in self.pState = .error(error) } + return } - .eraseToAnyPublisher() } } diff --git a/Sources/NFCDemo/NFC/NFCLoginController.swift b/Sources/NFCDemo/NFC/NFCLoginController.swift index 1c6bea9..6b195cb 100644 --- a/Sources/NFCDemo/NFC/NFCLoginController.swift +++ b/Sources/NFCDemo/NFC/NFCLoginController.swift @@ -18,6 +18,7 @@ import CardReaderProviderApi import Combine import CoreNFC import Foundation +import GemCommonsKit import HealthCardAccess import HealthCardControl import Helper @@ -26,7 +27,7 @@ import NFCCardReaderProvider public class NFCLoginController: LoginController { public enum Error: Swift.Error, LocalizedError { /// In case the PIN or CAN could not be constructed from input - case cardError(NFCTagReaderSession.Error) + case cardError(NFCHealthCardSessionError) case invalidCanOrPinFormat case wrongPin(retryCount: Int) case signatureFailure(ResponseStatus) @@ -54,6 +55,7 @@ public class NFCLoginController: LoginController { } } + @MainActor @Published private var pState: ViewState = .idle var state: Published>.Publisher { @@ -62,17 +64,17 @@ public class NFCLoginController: LoginController { var cancellable: AnyCancellable? - func dismissError() { + @MainActor + func dismissError() async { if pState.error != nil { - callOnMainThread { - self.pState = .idle - } + pState = .idle } } - let messages = NFCTagReaderSession.Messages( + let messages = NFCHealthCardSession.Messages( discoveryMessage: NSLocalizedString("nfc_txt_discoveryMessage", comment: ""), connectMessage: NSLocalizedString("nfc_txt_connectMessage", comment: ""), + secureChannelMessage: NSLocalizedString("nfc_txt_secureChannel", comment: ""), noCardMessage: NSLocalizedString("nfc_txt_noCardMessage", comment: ""), multipleCardsMessage: NSLocalizedString("nfc_txt_multipleCardsMessage", comment: ""), unsupportedCardMessage: NSLocalizedString("nfc_txt_unsupportedCardMessage", comment: ""), @@ -80,133 +82,93 @@ public class NFCLoginController: LoginController { ) // swiftlint:disable:next function_body_length - func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) { - if case .loading = pState { return } - callOnMainThread { + func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) async { + if case .loading = await pState { return } + Task { @MainActor in self.pState = .loading(nil) } - let canData: CAN let format2Pin: Format2Pin do { - canData = try CAN.from(Data(can.utf8)) format2Pin = try Format2Pin(pincode: pin) } catch { - callOnMainThread { + Task { @MainActor in self.pState = .error(Error.invalidCanOrPinFormat) } return } - cancellable = NFCTagReaderSession.publisher(messages: messages) - .mapError { Error.cardError($0) as Swift.Error } - .flatMap { (session: NFCCardSession) -> AnyPublisher, Swift.Error> in - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_secure_channel", comment: "")) - return session.card // swiftlint:disable:this trailing_closure - .openSecureSession(can: canData, writeTimeout: 0, readTimeout: 0) - .userMessage(session: session, message: NSLocalizedString("nfc_txt_msg_verify_pin", comment: "")) - .verifyPin(pin: format2Pin, type: EgkFileSystem.Pin.mrpinHome, in: session) - .userMessage(session: session, message: NSLocalizedString("nfc_txt_msg_signing", comment: "")) - .sign(payload: "ABC".data(using: .utf8)!, // swiftlint:disable:this force_unwrapping - in: session, - checkAlgorithm: checkBrainpoolAlgorithm) - .map { _ in true } - .map(ViewState.value) - .handleEvents(receiveOutput: { state in - if let value = state.value, value == true { - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_success", comment: "")) - session.invalidateSession(with: nil) - } else { - session.invalidateSession( - with: state.error?.localizedDescription ?? NSLocalizedString( - "nfc_txt_msg_failure", - comment: "" - ) - ) - } - }) - .mapError { error in - session.invalidateSession(with: error.localizedDescription) - return error - } - .eraseToAnyPublisher() + // tag::nfcHealthCardSession_init[] + guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_verify_pin", comment: "")) + let verifyPinResponse = try await session.card.verifyAsync( + pin: format2Pin, + type: EgkFileSystem.Pin.mrpinHome + ) + if case let VerifyPinResponse.wrongSecretWarning(retryCount: count) = verifyPinResponse { + throw NFCLoginController.Error.wrongPin(retryCount: count) + } else if case VerifyPinResponse.passwordBlocked = verifyPinResponse { + throw NFCLoginController.Error.passwordBlocked + } else if VerifyPinResponse.success != verifyPinResponse { + throw NFCLoginController.Error.verifyPinResponse } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - if case let .failure(error) = completion { - if case let .cardError(readerError) = error as? NFCLoginController.Error, - case let .nfcTag(error: tagError) = readerError, - case .userCanceled = tagError { - self?.pState = .idle - } else { - self?.pState = .error(error) - } - } else { - self?.pState = .idle - } - self?.cancellable?.cancel() - }, receiveValue: { [weak self] value in - self?.pState = value - }) - } -} -extension Publisher { - func userMessage(session: NFCCardSession, message: String) -> AnyPublisher { - handleEvents(receiveOutput: { _ in - // swiftlint:disable:previous trailing_closure - session.updateAlert(message: message) + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_signing", comment: "")) + let outcome = try await session.card.sign( + payload: "ABC".data(using: .utf8)!, // swiftlint:disable:this force_unwrapping + checkAlgorithm: checkBrainpoolAlgorithm + ) + + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_success", comment: "")) + return outcome }) - .eraseToAnyPublisher() + else { + // handle the case the Session could not be initialized + // end::nfcHealthCardSession_init[] + Task { @MainActor in self.pState = .error(NFCHealthCardSessionError.couldNotInitializeSession) } + return + } + + let signedData: Data + do { + // tag::nfcHealthCardSession_execute[] + signedData = try await nfcHealthCardSession.executeOperation() + // end::nfcHealthCardSession_execute[] + + Task { @MainActor in self.pState = .value(true) } + // tag::nfcHealthCardSession_errorHandling[] + } catch NFCHealthCardSessionError.coreNFC(.userCanceled) { + // error type is always `NFCHealthCardSessionError` + // here we especially handle when the user canceled the session + Task { @MainActor in self.pState = .idle } // Do some view-property update + // Calling .invalidateSession() is not strictly necessary + // since nfcHealthCardSession does it while it's de-initializing. + nfcHealthCardSession.invalidateSession(with: nil) + return + } catch { + Task { @MainActor in self.pState = .error(error) } + nfcHealthCardSession.invalidateSession(with: error.localizedDescription) + return + } + // end::nfcHealthCardSession_errorHandling[] + DLog("Signed Data: \(signedData)") } } -extension Publisher where Output == HealthCardType, Self.Failure == Swift.Error { - func verifyPin(pin: Format2Pin, - type: EgkFileSystem.Pin, - in _: NFCCardSession) -> AnyPublisher { - flatMap { secureCard in - secureCard.verify(pin: pin, type: type) - .tryMap { response in - if case let VerifyPinResponse.wrongSecretWarning(retryCount: count) = response { - throw NFCLoginController.Error.wrongPin(retryCount: count) - } - if case VerifyPinResponse.passwordBlocked = response { - throw NFCLoginController.Error.passwordBlocked - } - if response != VerifyPinResponse.success { - throw NFCLoginController.Error.verifyPinResponse - } - return secureCard - } - }.eraseToAnyPublisher() - } +extension HealthCardType { + func sign(payload: Data, checkAlgorithm: Bool) async throws -> Data { + let certificate = try await readAutCertificateAsync() + if checkAlgorithm, !certificate.info.algorithm.isBp256r1 { + throw NFCLoginController.Error.invalidAlgorithm(certificate.info.algorithm) + } - func sign(payload: Data, in _: NFCCardSession, checkAlgorithm: Bool) -> AnyPublisher { - flatMap { secureCard -> AnyPublisher in - secureCard - .readAutCertificate() - .flatMap { certificate -> AnyPublisher in - // Check AutCertificateResponse here ... - if checkAlgorithm, !certificate.info.algorithm.isBp256r1 { - return Fail(error: NFCLoginController.Error.invalidAlgorithm(certificate.info.algorithm)) - .eraseToAnyPublisher() - } - - CommandLogger.commands.append(Command(message: "Sign payload with card", type: .description)) - return secureCard.sign(data: payload) - .tryMap { response in - if response.responseStatus == ResponseStatus.success, let signature = response.data { - Swift.print("SIGNATURE: \(signature.hexString())") - return signature - } else { - throw NFCLoginController.Error.signatureFailure(response.responseStatus) - } - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() + CommandLogger.commands.append(Command(message: "Sign payload with card", type: .description)) + let response = try await signAsync(data: payload) + guard response.responseStatus == ResponseStatus.success, let signature = response.data + else { + throw NFCLoginController.Error.signatureFailure(response.responseStatus) } - .eraseToAnyPublisher() + Swift.print("SIGNATURE: \(signature.hexString())") + return signature } } diff --git a/Sources/NFCDemo/NFC/NFCResetRetryCounterController.swift b/Sources/NFCDemo/NFC/NFCResetRetryCounterController.swift index 4c353b0..7b9f8e5 100644 --- a/Sources/NFCDemo/NFC/NFCResetRetryCounterController.swift +++ b/Sources/NFCDemo/NFC/NFCResetRetryCounterController.swift @@ -48,6 +48,7 @@ public class NFCResetRetryCounterController: ResetRetryCounter { } } + @MainActor @Published private var pState: ViewState = .idle var state: Published>.Publisher { @@ -56,228 +57,138 @@ public class NFCResetRetryCounterController: ResetRetryCounter { var cancellable: AnyCancellable? - func dismissError() { + @MainActor + func dismissError() async { if pState.error != nil { - callOnMainThread { - self.pState = .idle - } + pState = .idle } } - let messages = NFCTagReaderSession.Messages( + let messages = NFCHealthCardSession.Messages( discoveryMessage: NSLocalizedString("nfc_txt_discoveryMessage", comment: ""), connectMessage: NSLocalizedString("nfc_txt_connectMessage", comment: ""), + secureChannelMessage: NSLocalizedString("nfc_txt_secureChannel", comment: ""), noCardMessage: NSLocalizedString("nfc_txt_noCardMessage", comment: ""), multipleCardsMessage: NSLocalizedString("nfc_txt_multipleCardsMessage", comment: ""), unsupportedCardMessage: NSLocalizedString("nfc_txt_unsupportedCardMessage", comment: ""), connectionErrorMessage: NSLocalizedString("nfc_txt_connectionErrorMessage", comment: "") ) - // swiftlint:disable:next function_body_length - func resetRetryCounter(can: String, puk: String) { - if case .loading = pState { return } - callOnMainThread { + func resetRetryCounter(can: String, puk: String) async { + if case .loading = await pState { return } + Task { @MainActor in self.pState = .loading(nil) } - let canData: CAN let format2Puk: Format2Pin do { - canData = try CAN.from(Data(can.utf8)) format2Puk = try Format2Pin(pincode: puk) } catch { - callOnMainThread { + Task { @MainActor in self.pState = .error(Error.invalidCanOrPinFormat) } return } - cancellable = NFCTagReaderSession.publisher(messages: messages) - .mapError { Error.cardError($0) as Swift.Error } - .flatMap { (session: NFCCardSession) -> AnyPublisher, Swift.Error> in - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_secure_channel", comment: "")) - return session.card // swiftlint:disable:this trailing_closure - .openSecureSession(can: canData, writeTimeout: 0, readTimeout: 0) - .userMessage( - session: session, - message: NSLocalizedString("nfc_txt_msg_reset_withoutNewPin", comment: "") - ) - .resetRetryCounter( - puk: format2Puk, - type: EgkFileSystem.Pin.mrpinHome, - dfSpecific: false - ) + guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_withoutNewPin", comment: "")) + let resetResponse = try await session.card.resetRetryCounter( + puk: format2Puk, + type: EgkFileSystem.Pin.mrpinHome, + dfSpecific: false + ) - .map { _ in true } - .map(ViewState.value) - .handleEvents(receiveOutput: { state in - if let value = state.value, value == true { - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) - session.invalidateSession(with: nil) - } else { - session.invalidateSession( - with: state.error?.localizedDescription ?? NSLocalizedString( - "nfc_txt_msg_failure", - comment: "" - ) - ) - } - }) - .mapError { error in - session.invalidateSession(with: error.localizedDescription) - return error - } - .eraseToAnyPublisher() - } - .receive(on: DispatchQueue.main) - .sink( - receiveCompletion: { [weak self] completion in - if case let .failure(error) = completion { - if case let .cardError(readerError) = error as? NFCResetRetryCounterController.Error, - case let .nfcTag(error: tagError) = readerError, - case .userCanceled = tagError { - self?.pState = .idle - } else { - self?.pState = .error(error) - } - } else { - self?.pState = .idle - } - self?.cancellable?.cancel() - }, - receiveValue: { [weak self] value in - self?.pState = value + if case ResetRetryCounterResponse.success = resetResponse { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) + return resetResponse + } else { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_failure", comment: "")) + if case let ResetRetryCounterResponse.wrongSecretWarning(retryCount: count) = resetResponse { + throw NFCResetRetryCounterController.Error.wrongPin(retryCount: count) } - ) + if case ResetRetryCounterResponse.commandBlocked = resetResponse { + throw NFCResetRetryCounterController.Error.commandBlocked + } + // else + throw NFCResetRetryCounterController.Error.otherError + } + }) + else { + Task { @MainActor in self.pState = .error(NFCTagReaderSession.Error.couldNotInitializeSession) } + return + } + + do { + _ = try await nfcHealthCardSession.executeOperation() + Task { @MainActor in self.pState = .value(true) } + } catch NFCHealthCardSessionError.coreNFC(.userCanceled) { + nfcHealthCardSession.invalidateSession(with: nil) + Task { @MainActor in self.pState = .idle } + return + } catch { + nfcHealthCardSession.invalidateSession(with: error.localizedDescription) + Task { @MainActor in self.pState = .error(error) } + return + } } // swiftlint:disable:next function_body_length - func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) { - if case .loading = pState { return } - callOnMainThread { + func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) async { + if case .loading = await pState { return } + Task { @MainActor in self.pState = .loading(nil) } - let canData: CAN let format2Puk: Format2Pin let format2Pin: Format2Pin do { - canData = try CAN.from(Data(can.utf8)) format2Puk = try Format2Pin(pincode: puk) format2Pin = try Format2Pin(pincode: newPin) } catch { - callOnMainThread { + Task { @MainActor in self.pState = .error(Error.invalidCanOrPinFormat) } return } - cancellable = NFCTagReaderSession.publisher(messages: messages) - .mapError { Error.cardError($0) as Swift.Error } - .flatMap { (session: NFCCardSession) -> AnyPublisher, Swift.Error> in - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_secure_channel", comment: "")) - return session.card // swiftlint:disable:this trailing_closure - .openSecureSession(can: canData, writeTimeout: 0, readTimeout: 0) - .userMessage( - session: session, - message: NSLocalizedString("nfc_txt_msg_reset_withNewPin", comment: "") - ) - .resetRetryCounterAndSetNewPin( - puk: format2Puk, - newPin: format2Pin, - type: EgkFileSystem.Pin.mrpinHome, - dfSpecific: false - ) - - .map { _ in true } - .map(ViewState.value) - .handleEvents(receiveOutput: { state in - if let value = state.value, value == true { - session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) - session.invalidateSession(with: nil) - } else { - session.invalidateSession( - with: state.error?.localizedDescription ?? NSLocalizedString( - "nfc_txt_msg_failure", - comment: "" - ) - ) - } - }) - .mapError { error in - session.invalidateSession(with: error.localizedDescription) - return error - } - .eraseToAnyPublisher() - } - .receive(on: DispatchQueue.main) - .sink(receiveCompletion: { [weak self] completion in - if case let .failure(error) = completion { - self?.pState = .error(error) - } else { - self?.pState = .idle - } - self?.cancellable?.cancel() - }, receiveValue: { [weak self] value in - self?.pState = value - }) - } -} - -extension Publisher where Output == HealthCardType, Self.Failure == Swift.Error { - func resetRetryCounter( - puk: Format2Pin, - type: EgkFileSystem.Pin, - dfSpecific: Bool - ) -> AnyPublisher { - flatMap { secureCard in - secureCard.resetRetryCounter( - puk: puk, - type: type, - dfSpecific: dfSpecific + guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_withNewPin", comment: "")) + let resetResponse = try await session.card.resetRetryCounterAndSetNewPinAsync( + puk: format2Puk, + newPin: format2Pin, + type: EgkFileSystem.Pin.mrpinHome, + dfSpecific: false ) - .tryMap { response in - if case ResetRetryCounterResponse.success = response { - return secureCard - } - if case let ResetRetryCounterResponse.wrongSecretWarning(retryCount: count) = response { + + if case ResetRetryCounterResponse.success = resetResponse { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_success", comment: "")) + return resetResponse + } else { + session.updateAlert(message: NSLocalizedString("nfc_txt_msg_reset_failure", comment: "")) + if case let ResetRetryCounterResponse.wrongSecretWarning(retryCount: count) = resetResponse { throw NFCResetRetryCounterController.Error.wrongPin(retryCount: count) } - if case ResetRetryCounterResponse.commandBlocked = response { + if case ResetRetryCounterResponse.commandBlocked = resetResponse { throw NFCResetRetryCounterController.Error.commandBlocked } // else throw NFCResetRetryCounterController.Error.otherError } + }) + else { + Task { @MainActor in self.pState = .error(NFCTagReaderSession.Error.couldNotInitializeSession) } + return } - .eraseToAnyPublisher() - } - func resetRetryCounterAndSetNewPin( - puk: Format2Pin, - newPin: Format2Pin, - type: EgkFileSystem.Pin, - dfSpecific: Bool - ) -> AnyPublisher { - flatMap { secureCard in - secureCard.resetRetryCounterAndSetNewPin( - puk: puk, - newPin: newPin, - type: type, - dfSpecific: dfSpecific - ) - .tryMap { response in - if case ResetRetryCounterResponse.success = response { - return secureCard - } - if case let ResetRetryCounterResponse.wrongSecretWarning(retryCount: count) = response { - throw NFCResetRetryCounterController.Error.wrongPin(retryCount: count) - } - if case ResetRetryCounterResponse.commandBlocked = response { - throw NFCResetRetryCounterController.Error.commandBlocked - } - // else - throw NFCResetRetryCounterController.Error.otherError - } + do { + _ = try await nfcHealthCardSession.executeOperation() + Task { @MainActor in self.pState = .value(true) } + } catch NFCHealthCardSessionError.coreNFC(.userCanceled) { + nfcHealthCardSession.invalidateSession(with: nil) + Task { @MainActor in self.pState = .idle } + return + } catch { + nfcHealthCardSession.invalidateSession(with: error.localizedDescription) + Task { @MainActor in self.pState = .error(error) } + return } - .eraseToAnyPublisher() } } diff --git a/Sources/NFCDemo/Resources/Base.lproj/Localizable.strings b/Sources/NFCDemo/Resources/Base.lproj/Localizable.strings index f7324a3..d0b7d94 100644 --- a/Sources/NFCDemo/Resources/Base.lproj/Localizable.strings +++ b/Sources/NFCDemo/Resources/Base.lproj/Localizable.strings @@ -63,6 +63,7 @@ "nfc_txt_discoveryMessage" = "Please tap your eGK on top of your iPhone"; "nfc_txt_connectMessage"= "Connecting"; +"nfc_txt_secureChannel" = "Establishing a secure connection"; "nfc_txt_noCardMessage"= "No Card Found"; "nfc_txt_multipleCardsMessage" = "Multiple cards found"; "nfc_txt_unsupportedCardMessage" = "Unsupported card"; diff --git a/Sources/NFCDemo/Resources/Info.plist b/Sources/NFCDemo/Resources/Info.plist index 29c64e3..8b87104 100644 --- a/Sources/NFCDemo/Resources/Info.plist +++ b/Sources/NFCDemo/Resources/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.2.4 + 1.2.5 CFBundleVersion $(GEMATIK_BUNDLE_VERSION) GEMATIKSourceVersion diff --git a/Sources/NFCDemo/Resources/de.lproj/Localizable.strings b/Sources/NFCDemo/Resources/de.lproj/Localizable.strings index e65fd59..aa05deb 100644 --- a/Sources/NFCDemo/Resources/de.lproj/Localizable.strings +++ b/Sources/NFCDemo/Resources/de.lproj/Localizable.strings @@ -63,6 +63,7 @@ "nfc_txt_discoveryMessage" = "Bitte legen Sie die Karte auf Ihr iPhone"; "nfc_txt_connectMessage" = "Verbinden"; +"nfc_txt_secureChannel" = "Herstellen einer sicheren Verbindung"; "nfc_txt_noCardMessage" = "Keine Karte gefunden"; "nfc_txt_multipleCardsMessage" = "Es wurden mehrere Karten gefunden"; "nfc_txt_unsupportedCardMessage" = "Karte wird nicht unterstĆ¼tzt"; diff --git a/Sources/NFCDemo/Screens/Registration/NFCChangeReferenceDataViewModel.swift b/Sources/NFCDemo/Screens/Registration/NFCChangeReferenceDataViewModel.swift index c5f0bf0..d412e01 100644 --- a/Sources/NFCDemo/Screens/Registration/NFCChangeReferenceDataViewModel.swift +++ b/Sources/NFCDemo/Screens/Registration/NFCChangeReferenceDataViewModel.swift @@ -23,9 +23,9 @@ import SwiftUI protocol ChangeReferenceData { var state: Published>.Publisher { get } - func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) + func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) async - func dismissError() + func dismissError() async } class NFCChangeReferenceDataViewModel: ObservableObject { @@ -55,11 +55,7 @@ class NFCChangeReferenceDataViewModel: ObservableObject { .store(in: &disposables) } - func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) { - changeReferenceDataController.changeReferenceDataSetNewPin(can: can, oldPin: oldPin, newPin: newPin) - } - - func dismissError() { - changeReferenceDataController.dismissError() + func changeReferenceDataSetNewPin(can: String, oldPin: String, newPin: String) async { + await changeReferenceDataController.changeReferenceDataSetNewPin(can: can, oldPin: oldPin, newPin: newPin) } } diff --git a/Sources/NFCDemo/Screens/Registration/NFCLoginViewModel.swift b/Sources/NFCDemo/Screens/Registration/NFCLoginViewModel.swift index 89c2ddf..f773e7a 100644 --- a/Sources/NFCDemo/Screens/Registration/NFCLoginViewModel.swift +++ b/Sources/NFCDemo/Screens/Registration/NFCLoginViewModel.swift @@ -23,8 +23,8 @@ import SwiftUI protocol LoginController { var state: Published>.Publisher { get } - func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) - func dismissError() + func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) async + func dismissError() async } class NFCLoginViewModel: ObservableObject { @@ -52,12 +52,12 @@ class NFCLoginViewModel: ObservableObject { .store(in: &disposables) } - func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) { - loginController.login(can: can, pin: pin, checkBrainpoolAlgorithm: checkBrainpoolAlgorithm) + func login(can: String, pin: String, checkBrainpoolAlgorithm: Bool) async { + await loginController.login(can: can, pin: pin, checkBrainpoolAlgorithm: checkBrainpoolAlgorithm) } - func dismissError() { - loginController.dismissError() + func dismissError() async { + await loginController.dismissError() } } diff --git a/Sources/NFCDemo/Screens/Registration/NFCResetRetryCounterViewModel.swift b/Sources/NFCDemo/Screens/Registration/NFCResetRetryCounterViewModel.swift index d83e3f4..15ce3d7 100644 --- a/Sources/NFCDemo/Screens/Registration/NFCResetRetryCounterViewModel.swift +++ b/Sources/NFCDemo/Screens/Registration/NFCResetRetryCounterViewModel.swift @@ -23,11 +23,11 @@ import SwiftUI protocol ResetRetryCounter { var state: Published>.Publisher { get } - func resetRetryCounter(can: String, puk: String) + func resetRetryCounter(can: String, puk: String) async - func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) + func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) async - func dismissError() + func dismissError() async } class NFCResetRetryCounterViewModel: ObservableObject { @@ -57,15 +57,11 @@ class NFCResetRetryCounterViewModel: ObservableObject { .store(in: &disposables) } - func resetRetryCounter(can: String, puk: String) { - resetRetryCounterController.resetRetryCounter(can: can, puk: puk) + func resetRetryCounter(can: String, puk: String) async { + await resetRetryCounterController.resetRetryCounter(can: can, puk: puk) } - func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) { - resetRetryCounterController.resetRetryCounterWithNewPin(can: can, puk: puk, newPin: newPin) - } - - func dismissError() { - resetRetryCounterController.dismissError() + func resetRetryCounterWithNewPin(can: String, puk: String, newPin: String) async { + await resetRetryCounterController.resetRetryCounterWithNewPin(can: can, puk: puk, newPin: newPin) } } diff --git a/Sources/NFCDemo/Screens/Registration/StartNFCView.swift b/Sources/NFCDemo/Screens/Registration/StartNFCView.swift index c4a6f1d..82c6194 100644 --- a/Sources/NFCDemo/Screens/Registration/StartNFCView.swift +++ b/Sources/NFCDemo/Screens/Registration/StartNFCView.swift @@ -100,13 +100,21 @@ struct StartNFCView: View { Divider() Button { - switch useCase { - case .login: loginState.login(can: can, pin: pin, checkBrainpoolAlgorithm: checkBrainpoolAlgorithm) - case .resetRetryCounter: resetRetryCounterState.resetRetryCounter(can: can, puk: puk) - case .resetRetryCounterWithNewPin: resetRetryCounterState - .resetRetryCounterWithNewPin(can: can, puk: puk, newPin: pin) - case .changeReferenceDataSetNewPin: changeReferenceDataState - .changeReferenceDataSetNewPin(can: can, oldPin: oldPin, newPin: pin) + Task { + switch useCase { + case .login: + await loginState.login(can: can, pin: pin, checkBrainpoolAlgorithm: checkBrainpoolAlgorithm) + case .resetRetryCounter: + await resetRetryCounterState.resetRetryCounter(can: can, puk: puk) + case .resetRetryCounterWithNewPin: + await resetRetryCounterState.resetRetryCounterWithNewPin(can: can, puk: puk, newPin: pin) + case .changeReferenceDataSetNewPin: + await changeReferenceDataState.changeReferenceDataSetNewPin( + can: can, + oldPin: oldPin, + newPin: pin + ) + } } } label: { @@ -142,7 +150,9 @@ struct StartNFCView: View { title: Text("alert_error_title"), message: Text(error?.localizedDescription ?? "alert_error_message_unknown"), dismissButton: .default(Text("alert_btn_ok")) { - self.loginState.dismissError() + Task { + await self.loginState.dismissError() + } } ) } diff --git a/Tests/HealthCardControlTests/SecureMessaging/HealthCardTypeExtESIGNTest.swift b/Tests/HealthCardControlTests/SecureMessaging/HealthCardTypeExtESIGNTest.swift index c13d03c..ec62ba3 100644 --- a/Tests/HealthCardControlTests/SecureMessaging/HealthCardTypeExtESIGNTest.swift +++ b/Tests/HealthCardControlTests/SecureMessaging/HealthCardTypeExtESIGNTest.swift @@ -42,13 +42,19 @@ final class HealthCardTypeExtESIGNTest: XCTestCase { handler = messageHandler } - func transmit(command: CommandType, writeTimeout: TimeInterval, readTimeout: TimeInterval) - throws -> ResponseType { + func transmit( + command: CommandType, + writeTimeout: TimeInterval, + readTimeout: TimeInterval + ) throws -> ResponseType { try handler(command, self, writeTimeout, readTimeout) } - func transmitAsync(command: CommandType, writeTimeout: TimeInterval, readTimeout: TimeInterval) - throws -> ResponseType { + func transmitAsync( + command: CommandType, + writeTimeout: TimeInterval, + readTimeout: TimeInterval + ) async throws -> ResponseType { try handler(command, self, writeTimeout, readTimeout) } @@ -152,7 +158,7 @@ final class HealthCardTypeExtESIGNTest: XCTestCase { let card = MockHealthCard(status: egkCardStatus, currentCardChannel: channel) var autCertificateResponse: AutCertificateResponse? - autCertificateResponse = try await card.readAutCertificate() + autCertificateResponse = try await card.readAutCertificateAsync() expect(autCertificateResponse?.info) == .efAutE256 expect(autCertificateResponse?.certificate) == mockCertificate } diff --git a/doc/userguide/OHCKIT_GettingStarted.adoc b/doc/userguide/OHCKIT_GettingStarted.adoc index 888307e..402ea4c 100644 --- a/doc/userguide/OHCKIT_GettingStarted.adoc +++ b/doc/userguide/OHCKIT_GettingStarted.adoc @@ -1,12 +1,12 @@ == Getting Started -OpenHealthCardKit requires Swift 5.1. +OpenHealthCardKit requires Swift 5.6. === Setup for integration - **Swift Package Manager:** Put this in your `Package.swift`: - `.package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.3.0"),` + `.package(url: "https://github.com/gematik/ref-OpenHealthCardKit", from: "5.6.0"),` - **Carthage:** Put this in your `Cartfile`: diff --git a/doc/userguide/OHCKIT_HealthCardAccess.adoc b/doc/userguide/OHCKIT_HealthCardAccess.adoc index a0024a4..8158573 100644 --- a/doc/userguide/OHCKIT_HealthCardAccess.adoc +++ b/doc/userguide/OHCKIT_HealthCardAccess.adoc @@ -50,25 +50,27 @@ by setting the APDU-bytes manually. include::{integrationtestdir}/HealthCardAccess/PublisherIntegrationTest.swift[tags=createCommand,indent=0] ---- -===== Setting an execution target +===== Command execution We execute the created command `CardType` instance which has been typically provided by a `CardReaderType`. In the next example we use a `HealthCard` object representing an eGK (elektronische Gesundheitskarte) -as one kind of a `HealthCardType` implementing the `CardType` protocol. - +as one kind of a `HealthCardType` implementing the `CardType` protocol and then send the command to the card (or card's channel): [source,swift] ---- -include::{integrationtestdir}/HealthCardAccess/PublisherIntegrationTest.swift[tags=setExecutionTarget,indent=0] +include::{integrationtestdir}/HealthCardAccess/PublisherIntegrationTest.swift[tags=evaluateResponseStatus,indent=0] ---- + +*Following paragraphs describe the deprecated way of executung commands via the _Combine_ inteface:* + A created command can be lifted to the Combine framework with `publisher(for:writetimeout:readtimeout)`. The result of the command execution can be validated against an expected `ResponseStatus`, e.g. +SUCCESS+ (+0x9000+). [source,swift] ---- -include::{integrationtestdir}/HealthCardAccess/PublisherIntegrationTest.swift[tags=evaluateResponseStatus,indent=0] +include::{integrationtestdir}/HealthCardAccess/PublisherIntegrationTest.swift[tags=evaluateResponseStatus_publisher,indent=0] ---- ===== Create a Command Sequence diff --git a/doc/userguide/OHCKIT_HealthCardControl.adoc b/doc/userguide/OHCKIT_HealthCardControl.adoc index 0811d15..ae62ecb 100644 --- a/doc/userguide/OHCKIT_HealthCardControl.adoc +++ b/doc/userguide/OHCKIT_HealthCardControl.adoc @@ -19,7 +19,7 @@ Take the necessary preparatory steps for signing a challenge on the Health Card, [source,swift] ---- -include::{integrationtestdir}/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift[tags=signChallenge_publisher,indent=0] +include::{integrationtestdir}/HealthCardControl/HealthCardTypeExtESIGNIntegrationTest.swift[tags=signChallenge,indent=0] ---- @@ -28,7 +28,7 @@ steps for establishing a secure channel with the Health Card and expose only a s [source,swift] ---- -include::{integrationtestdir}/HealthCardControl/KeyAgreementIntegrationTest.swift[tags=negotiateSessionKey_publisher,indent=0] +include::{integrationtestdir}/HealthCardControl/KeyAgreementIntegrationTest.swift[tags=negotiateSessionKey,indent=0] ---- See the integration tests link:include::{integrationtestdir}/HealthCardControl/[IntegrationTests/HealthCardControl/] diff --git a/doc/userguide/OHCKIT_NFCCardReaderProvider.adoc b/doc/userguide/OHCKIT_NFCCardReaderProvider.adoc index 09b2d20..ff55f28 100644 --- a/doc/userguide/OHCKIT_NFCCardReaderProvider.adoc +++ b/doc/userguide/OHCKIT_NFCCardReaderProvider.adoc @@ -2,4 +2,30 @@ === NFCCardReaderProvider A `CardReaderProvider` implementation that handles the -communication with the Apple iPhone NFC interface. \ No newline at end of file +communication with the Apple iPhone NFC interface. + +==== NFCCardReaderSession + +For convience, the `NFCCardReaderSession` combines the usage of the NFC inteface with the `HealthCardAccess/HealthCardControl` layers. + +The initializer takes some NFC-Display messages, the CAN (card access number) and a closure with a `NFCHealthCardSessionHandle` to send/receive commands/responses to/from the NFC HealthCard and to update the user's interface message to. + +[source,swift] +---- +include::{sourcedir}/NFCDemo/NFC/NFCLoginController.swift[tags=nfcHealthCardSession_init,indent=0] +---- + +Execute the operation on the NFC HealthCard. The secure channel (PACE) is established initially before executing the operation. + +[source,swift] +---- +include::{sourcedir}/NFCDemo/NFC/NFCLoginController.swift[tags=nfcHealthCardSession_execute,indent=0] +---- + +The thrown error will be of type `NFCHealthCardSessionError`. +The `NFCHealthCardSession` also gives you an endpoint to invalidate the underlying `TagReaderSession`. + +[source,swift] +---- +include::{sourcedir}/NFCDemo/NFC/NFCLoginController.swift[tags=nfcHealthCardSession_errorHandling,indent=0] +---- \ No newline at end of file diff --git a/project.yml b/project.yml index ed8e1d8..010e2bf 100644 --- a/project.yml +++ b/project.yml @@ -268,6 +268,7 @@ targets: - Sources/NFCCardReaderProvider dependencies: - target: HealthCardAccess_iOS + - target: HealthCardControl_iOS - target: Helper_iOS - package: DataKit - package: GemCommonsKit diff --git a/scripts/bootstrap b/scripts/bootstrap index ce44c39..dc08e56 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -27,3 +27,7 @@ if [ -f "Gemfile" ]; then echo "==> Installing gem dependenciesā€¦" bundle install --system fi + +if [ -f "Mintfile" ]; then + mint bootstrap +fi