Skip to content

Commit

Permalink
Extend Demo App by "Read personal data" use case (#36)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigabrtz authored Oct 21, 2024
1 parent 4230060 commit 0fb3926
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 1 deletion.
28 changes: 28 additions & 0 deletions Sources/NFCDemo/EnvironmentValues+ReadPersonalDataController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// 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.
//

import SwiftUI

struct ReadPersonalDataControllerKey: EnvironmentKey {
static let defaultValue: ReadPersonalData = NFCReadPersonalDataController()
}

extension EnvironmentValues {
var readPersonalDataController: ReadPersonalData {
get { self[ReadPersonalDataControllerKey.self] }
set { self[ReadPersonalDataControllerKey.self] = newValue }
}
}
153 changes: 153 additions & 0 deletions Sources/NFCDemo/NFC/NFCReadPersonalDataController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// 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.
//

import CardReaderProviderApi
import Combine
import CoreNFC
import Foundation
import Gzip
import HealthCardAccess
import HealthCardControl
import Helper
import NFCCardReaderProvider
import SwiftyXMLParser
public class NFCReadPersonalDataController: ReadPersonalData {
public enum Error: Swift.Error, LocalizedError {
/// In case the PIN, PUK or CAN could not be constructed from input
case cardError(NFCTagReaderSession.Error)
case invalidCanOrPinFormat
case commandBlocked
case otherError

public var errorDescription: String? {
switch self {
case let .cardError(error):
return error.localizedDescription
case .invalidCanOrPinFormat:
return "Invalid CAN, PUK or PIN format"
case .commandBlocked:
return "PUK cannot be used anymore! \n (PUK usage counter exhausted)"
case .otherError:
return "An unexpected error occurred."
}
}
}

@MainActor
@Published
private var pState: ViewState<PersonalData, Swift.Error> = .idle
var state: Published<ViewState<PersonalData, Swift.Error>>.Publisher {
$pState
}

var cancellable: AnyCancellable?

@MainActor
func dismissError() async {
if pState.error != nil {
pState = .idle
}
}

let messages = NFCHealthCardSession<Data>.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 readPersonalData(can: String) async {
if case .loading = await pState { return }
Task { @MainActor in
self.pState = .loading(nil)
}

guard let nfcHealthCardSession = NFCHealthCardSession(messages: messages, can: can, operation: { session in
session.updateAlert(message: NSLocalizedString("nfc_txt_meg_reading_personal_data", comment: ""))
let hcaApplicationIdentifier = EgkFileSystem.DF.HCA.aid

let hcaPdFileIdentifier = EgkFileSystem.EF.hcaPD.fid

_ = try await session.card
.selectDedicatedAsync(file: DedicatedFile(aid: hcaApplicationIdentifier, fid: hcaPdFileIdentifier))

let data = try await session.card.readSelectedFileAsync(
expected: nil,
failOnEndOfFileWarning: false,
offset: 0
)

return data
})
else {
Task { @MainActor in self.pState = .error(NFCTagReaderSession.Error.couldNotInitializeSession) }
return
}

do {
let personalDataData = try await nfcHealthCardSession.executeOperation()

// Personal data is compressed with gzip
// first 2 bytes indicate the length of the compressed data
// refer to https://gemspec.gematik.de/docs/gemSpec/gemSpec_eGK_Fach_VSDM/gemSpec_eGK_Fach_VSDM_V1.2.1/#2.4
let lengthBytes = personalDataData.prefix(2)
let length = UInt16(lengthBytes.withUnsafeBytes { $0.load(as: UInt16.self) })
let personalDataGzip = personalDataData.suffix(from: 2).prefix(Int(length))

let decompressedData: Data
if personalDataGzip.isGzipped {
decompressedData = try personalDataGzip.gunzipped()
} else {
decompressedData = personalDataGzip
}

// Data is now in xml format
// refer to xml schema definition file UC_PersoenlicheVersichertendatenXML.xsd in
// https://fachportal.gematik.de/schnelleinstieg/downloadcenter/schemadateien-wsdl-und-andere-dateien
// -> Schnittstellen­definitionen im XSD- und WSDL-Format für den PTV3-Konnektor
let personalData: PersonalData
let xml = XML.parse(decompressedData)
let insurantAccessor = xml["UC_PersoenlicheVersichertendatenXML"]["Versicherter"]
if let versichertenId = insurantAccessor["Versicherten_ID"].element?.text,
let firstName = insurantAccessor["Person"]["Vorname"].element?.text,
let surname = insurantAccessor["Person"]["Nachname"].element?.text,
let address = insurantAccessor["Person"]["StrassenAdresse"]["Ort"].element?.text {
personalData = PersonalData(
name: surname,
firstName: firstName,
address: address,
insuranceNumber: versichertenId
)
} else {
personalData = PersonalData.dummy
}

Task { @MainActor in self.pState = .value(personalData) }
} 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
}
}
}
13 changes: 13 additions & 0 deletions Sources/NFCDemo/Resources/Base.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"can_btn_next_login_test" = "Login-Test";
"can_btn_next_reset_pin" = "Unlock card";
"can_btn_next_reset_pin_with_new_pin" = "Set custom PIN";
"can_btn_next_read_personal_data" = "Read personal data";
"can_txt_help_title" = "Where can I find my CAN?";
"can_txt_help_explanation" = "Don't see a CAN printed on your card?\nThen you need a new health insurance card. You can request one from your statutory health insurer.";

Expand Down Expand Up @@ -46,6 +47,17 @@
"change_edt_enter_new_pin" = "New PIN";
"change_btn_next" = "Next";

/*
PERSONAL DATA
*/
"pd_txt_title" = "Read personal data";
"pd_txt_intro" = "Personal data will be presented after successfully reading the card. No PIN is needed.";
"pd_btn_next" = "Next";
"pd_lbl_name" = "Name";
"pd_lbl_first_name" = "First Name";
"pd_lbl_address" = "Address";
"pd_lbl_insurance_number" = "Insurance Number";

/*
StartNFC
*/
Expand All @@ -71,6 +83,7 @@
"nfc_txt_msg_secure_channel" = "Establishing secure connection..";
"nfc_txt_msg_verify_pin" = "Verifying pin...";
"nfc_txt_msg_reading_auth_cert"= "Reading authentication certificate...";
"nfc_txt_meg_reading_personal_data" = "Reading personal data...";
"nfc_txt_msg_signing" = "Signing...";
"nfc_txt_msg_success" = "Successful signing!";
"nfc_txt_msg_failure" = "Error!";
Expand Down
13 changes: 13 additions & 0 deletions Sources/NFCDemo/Resources/de.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"can_btn_next_login_test" = "Login-Test";
"can_btn_next_reset_pin" = "Karte entsperren";
"can_btn_next_reset_pin_with_new_pin" = "Wunsch-PIN vergeben";
"can_btn_next_read_personal_data" = "Persönliche Daten lesen";
"can_txt_help_title" = "Wo kann ich meine CAN finden?";
"can_txt_help_explanation" = "Falls sie auf Ihrer Karte keine Zugangsnummer (CAN) sehen, benötigen Sie eine neue Gesundheitskarte mit NFC-Chip.";

Expand Down Expand Up @@ -46,6 +47,17 @@
"change_edt_enter_new_pin" = "Neue Wunsch-PIN";
"change_btn_next" = "Weiter";

/*
PERSONAL DATA
*/
"pd_txt_title" = "Persönliche Daten lesen";
"pd_txt_intro" = "Die persönlichen Daten werden nach dem erfolgreichen Einlesen der Karte angezeigt. Eine PIN ist nicht erforderlich.";
"pd_btn_next" = "Weiter";
"pd_lbl_name" = "Name";
"pd_lbl_first_name" = "Vorname";
"pd_lbl_address" = "Adresse";
"pd_lbl_insurance_number" = "Versichertennummer";

/*
StartNFC
*/
Expand All @@ -71,6 +83,7 @@
"nfc_txt_msg_secure_channel" = "Sichere Verbindung aufbauen...";
"nfc_txt_msg_verify_pin" = "PIN verifizieren...";
"nfc_txt_msg_reading_auth_cert"= "Zertifikat lesen...";
"nfc_txt_meg_reading_personal_data" = "Lese persönliche Daten...";
"nfc_txt_msg_signing" = "Signieren...";
"nfc_txt_msg_success" = "Erfolgreich signiert!";
"nfc_txt_msg_failure" = "Fehler!";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// 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.
//

import Combine
import Foundation
import Helper
import NFCCardReaderProvider
import SwiftUI

struct PersonalData {
let name: String
let firstName: String
let address: String
let insuranceNumber: String

static let dummy = PersonalData(
name: "Dummy Data",
firstName: "Max",
address: "Musterstraße 1, 12345 Musterstadt",
insuranceNumber: "A123456789"
)
}

protocol ReadPersonalData {
var state: Published<ViewState<PersonalData, Error>>.Publisher { get }

func readPersonalData(can: String) async

func dismissError() async
}

class NFCReadPersonalDataViewModel: ObservableObject, Observable {
@Environment(\.readPersonalDataController) var readPersonalDataController: ReadPersonalData
@Published var state: ViewState<PersonalData, Error> = .idle
@Published var results: [ReadingResult] = []

private var disposables = Set<AnyCancellable>()

init(state: ViewState<PersonalData, Error> = .idle) {
self.state = state
results = results
readPersonalDataController.state
.dropFirst()
.sink { [weak self] viewState in
self?.state = viewState

guard !viewState.isLoading, !viewState.isIdle else {
return
}
}
.store(in: &disposables)
}

func readPersonalData(can: String) async {
await readPersonalDataController.readPersonalData(can: can)
}
}
Loading

0 comments on commit 0fb3926

Please sign in to comment.