diff --git a/AA2SDKWrapper.swift b/AA2SDKWrapper.swift new file mode 100644 index 0000000..a5113d9 --- /dev/null +++ b/AA2SDKWrapper.swift @@ -0,0 +1,11 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation +import SwiftUI + +@available(iOS 13, *) +public enum AA2SDKWrapper { + public static let workflowController = WorkflowController(withConnection: AA2SdkConnection.shared) +} diff --git a/card/core/AusweisApp2/AA2SdkConnection.swift b/card/core/AusweisApp2/AA2SdkConnection.swift new file mode 100644 index 0000000..de3f3e7 --- /dev/null +++ b/card/core/AusweisApp2/AA2SdkConnection.swift @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import AusweisApp2 +import Foundation + +class AA2SdkConnection: SdkConnection { + static let shared = AA2SdkConnection() + + var isStarted: Bool { + ausweisapp2_is_running() + } + + var onConnected: (() -> Void)? + var onMessageReceived: ((_ message: AA2Message) -> Void)? + + private let ausweisApp2Callback: AusweisApp2Callback = { (msg: UnsafePointer?) in + guard let msg = msg else { + AA2SdkConnection.shared.onNewMessage(messageJson: nil) + return + } + let messageString = String(cString: msg) + AA2SdkConnection.shared.onNewMessage(messageJson: messageString) + } + + private let jsonDecoder = JSONDecoder() + private let jsonEncoder = JSONEncoder() + + private init() {} + + func start() { + ausweisapp2_init(ausweisApp2Callback, nil) + } + + func stop() { + ausweisapp2_shutdown() + } + + func send(command: T) { + do { + let messageData = try jsonEncoder.encode(command) + if let messageJson = String(data: messageData, encoding: .utf8) { + print("Send message: \(messageJson)") + ausweisapp2_send(messageJson) + } + } catch { + print("Could not send json message") + } + } + + private func onNewMessage(messageJson: String?) { + guard let messageJson = messageJson else { + if let onConnected = onConnected { + onConnected() + } + return + } + + print("Received message: \(messageJson)") + do { + let messageData = Data(messageJson.utf8) + let message = try jsonDecoder.decode(AA2Message.self, from: messageData) + + if let onMessageReceived = onMessageReceived { + onMessageReceived(message) + } + } catch { + print("Could not parse json message") + } + } +} diff --git a/card/core/AusweisApp2/MessageExtensions.swift b/card/core/AusweisApp2/MessageExtensions.swift new file mode 100644 index 0000000..e6bcaa1 --- /dev/null +++ b/card/core/AusweisApp2/MessageExtensions.swift @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +extension AA2Message { + var aa2DateFormat: String { "yyyy-MM-dd" } + + func getCertificateDescription() -> CertificateDescription? { + guard + let description = description, + let validity = validity, + let effectiveDate = validity.effectiveDate.parseDate(format: aa2DateFormat), + let expirationDate = validity.expirationDate.parseDate(format: aa2DateFormat) + else { return nil } + + return CertificateDescription( + issuerName: description.issuerName, + issuerUrl: URL(string: description.issuerUrl), + purpose: description.purpose, + subjectName: description.subjectName, + subjectUrl: URL(string: description.subjectUrl), + termsOfUsage: description.termsOfUsage, + validity: CertificateValidity( + effectiveDate: effectiveDate, + expirationDate: expirationDate + ) + ) + } + + func getReaders() -> [Reader]? { + guard let readers = readers else { return nil } + + let readerList = readers.compactMap { reader -> Reader? in Reader(reader: reader) + } + return readerList + } + + func getReader() -> Reader? { + if let reader = reader { + return Reader(reader: reader) + } + + guard let name = name else { return nil } + guard let insertable = insertable else { return nil } + guard let attached = attached else { return nil } + guard let keypad = keypad else { return nil } + + return Reader( + name: name, + insertable: insertable, + attached: attached, + keypad: keypad, + card: getCard() + ) + } + + func getCard() -> Card? { + guard let card = card ?? reader?.card else { return nil } + + return Card(card: card) + } + + func getAccessRights() -> AccessRights? { + guard let chat = chat else { return nil } + + var auxiliaryData: AuxiliaryData? + if let aux = aux { + auxiliaryData = AuxiliaryData( + ageVerificationDate: aux.ageVerificationDate?.parseDate(format: aa2DateFormat), + requiredAge: Int(aux.requiredAge ?? ""), + validityDate: aux.validityDate?.parseDate(format: aa2DateFormat), + communityId: aux.communityId + ) + } + + let requiredRights = chat.required.compactMap { accessRight -> AccessRight? in + AccessRight(rawValue: accessRight) + } + let optionalRights = chat.optional.compactMap { accessRight -> AccessRight? in + AccessRight(rawValue: accessRight) + } + let effectiveRights = chat.effective.compactMap { accessRight -> AccessRight? in + AccessRight(rawValue: accessRight) + } + + return AccessRights( + requiredRights: requiredRights, + optionalRights: optionalRights, + effectiveRights: effectiveRights, + transactionInfo: transactionInfo, + auxiliaryData: auxiliaryData + ) + } + + func getAuthResult() -> AuthResult? { + let result = getAuthResultData() + + var resultUrl: URL? + if let url = url { + resultUrl = URL(string: url) + } + + if result != nil || resultUrl != nil { + return AuthResult( + url: resultUrl, + result: result + ) + } + + return nil + } + + func getAuthResultData() -> AuthResultData? { + guard let major = result?.major else { return nil } + + let minor = result?.minor + let description = result?.description + let message = result?.message + let reason = result?.reason + let language = result?.language + + return AuthResultData( + major: major, + minor: minor, + language: language, + description: description, + message: message, + reason: reason + ) + } +} diff --git a/card/core/AusweisApp2/protocol/Commands.swift b/card/core/AusweisApp2/protocol/Commands.swift new file mode 100644 index 0000000..419d28c --- /dev/null +++ b/card/core/AusweisApp2/protocol/Commands.swift @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +protocol Command: Encodable { + var cmd: String { get } +} + +struct Accept: Command { + let cmd = "ACCEPT" +} + +struct Cancel: Command { + let cmd = "CANCEL" +} + +struct GetCertificate: Command { + let cmd = "GET_CERTIFICATE" +} + +struct RunAuth: Command { + let cmd = "RUN_AUTH" + let tcTokenURL: String + let developerMode: Bool + let messages: AA2UserInfoMessages? + let status: Bool +} + +struct RunChangePin: Command { + let cmd = "RUN_CHANGE_PIN" + let messages: AA2UserInfoMessages? + let status: Bool +} + +struct SetAccessRights: Command { + let cmd = "SET_ACCESS_RIGHTS" + let chat: [String] +} + +struct GetAccessRights: Command { + let cmd = "GET_ACCESS_RIGHTS" +} + +struct SetCan: Command { + let cmd = "SET_CAN" + let value: String? +} + +struct SetPin: Command { + let cmd = "SET_PIN" + let value: String? +} + +struct SetNewPin: Command { + let cmd = "SET_NEW_PIN" + let value: String? +} + +struct SetPuk: Command { + let cmd = "SET_PUK" + let value: String? +} + +struct Interrupt: Command { + let cmd = "INTERRUPT" +} + +struct GetStatus: Command { + let cmd = "GET_STATUS" +} + +struct GetInfo: Command { + let cmd = "GET_INFO" +} + +struct GetReader: Command { + let cmd = "GET_READER" + let name: String +} + +struct GetReaderList: Command { + let cmd = "GET_READER_LIST" +} + +struct GetApiLevel: Command { + let cmd = "GET_API_LEVEL" +} + +struct SetApiLevel: Command { + let cmd = "SET_API_LEVEL" + let level: Int +} + +struct SetCard: Command { + let cmd = "SET_CARD" + let name: String + let simulator: Simulator? +} diff --git a/card/core/AusweisApp2/protocol/Messages.swift b/card/core/AusweisApp2/protocol/Messages.swift new file mode 100644 index 0000000..1c7f05a --- /dev/null +++ b/card/core/AusweisApp2/protocol/Messages.swift @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +struct AA2Messages { + static let MsgAccessRights = "ACCESS_RIGHTS" + static let MsgAuth = "AUTH" + static let MsgCertificate = "CERTIFICATE" + static let MsgChangePin = "CHANGE_PIN" + static let MsgEnterPin = "ENTER_PIN" + static let MsgEnterNewPin = "ENTER_NEW_PIN" + static let MsgEnterPuk = "ENTER_PUK" + static let MsgEnterCan = "ENTER_CAN" + static let MsgInsertCard = "INSERT_CARD" + static let MsgBadState = "BAD_STATE" + static let MsgReader = "READER" + static let MsgInvalid = "INVALID" + static let MsgUnknowCommand = "UNKNOWN_COMMAND" + static let MsgInternalError = "INTERNAL_ERROR" + static let MsgStatus = "STATUS" + static let MsgInfo = "INFO" + static let MsgReaderList = "READER_LIST" + static let MsgApiLevel = "API_LEVEL" +} + +struct AA2Message: Decodable { + let msg: String + let error: String? + let card: AA2Card? + let result: AA2Result? + let chat: AA2Chat? + let aux: AA2Aux? + let transactionInfo: String? + let validity: AA2Validity? + let description: AA2Description? + let url: String? + let success: Bool? + let reader: AA2Reader? + let readers: [AA2Reader]? + let name: String? + let insertable: Bool? + let attached: Bool? + let keypad: Bool? + let workflow: String? + let progress: Int? + let state: String? + let versionInfo: AA2VersionInfo? + let available: [Int]? + let current: Int? + + enum CodingKeys: String, CodingKey { + case versionInfo = "VersionInfo" + + case msg, error, card, result, chat, aux, transactionInfo, validity + case description, url, success, reader, workflow, progress, state + case name, insertable, attached, keypad, readers, available, current + } +} + +struct AA2Description: Decodable { + let issuerName: String + let issuerUrl: String + let purpose: String + let subjectName: String + let subjectUrl: String + let termsOfUsage: String +} + +struct AA2Validity: Decodable { + let effectiveDate: String + let expirationDate: String +} + +struct AA2Chat: Decodable { + let effective: [String] + let optional: [String] + let required: [String] +} + +struct AA2Aux: Decodable { + let ageVerificationDate: String? + let requiredAge: String? + let validityDate: String? + let communityId: String? +} + +struct AA2Card: Decodable { + let deactivated: Bool + let inoperative: Bool + let retryCounter: Int +} + +struct AA2Reader: Decodable { + let name: String + let insertable: Bool + let attached: Bool + let keypad: Bool + let card: AA2Card? +} + +struct AA2Result: Decodable { + let major: String? + let minor: String? + let url: String? + let language: String? + let description: String? + let message: String? + let reason: String? +} + +struct AA2VersionInfo: Decodable { + let name: String + let implementationTitle: String + let implementationVendor: String + let implementationVersion: String + let specificationTitle: String + let specificationVendor: String + let specificationVersion: String + + enum CodingKeys: String, CodingKey { + case name = "Name" + case implementationTitle = "Implementation-Title" + case implementationVendor = "Implementation-Vendor" + case implementationVersion = "Implementation-Version" + case specificationTitle = "Specification-Title" + case specificationVendor = "Specification-Vendor" + case specificationVersion = "Specification-Version" + } +} diff --git a/card/core/SdkConnection.swift b/card/core/SdkConnection.swift new file mode 100644 index 0000000..bd9c574 --- /dev/null +++ b/card/core/SdkConnection.swift @@ -0,0 +1,15 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +protocol SdkConnection { + var isStarted: Bool { get } + var onConnected: (() -> Void)? { get set } + var onMessageReceived: ((_ message: AA2Message) -> Void)? { get set } + + func start() + func stop() + func send(command: T) +} diff --git a/card/core/WorkflowCallbacks.swift b/card/core/WorkflowCallbacks.swift new file mode 100644 index 0000000..7756e67 --- /dev/null +++ b/card/core/WorkflowCallbacks.swift @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +/** + Authentication workflow callbacks. + + You need to register them with the WorkflowController + + See WorkflowController.registerCallbacks + */ +public protocol WorkflowCallbacks: AnyObject { + // swiftformat:sort:begin + + /** + Access rights requested in response to an authentication. + + This function will be called once the authentication is started by WorkflowController.startAuthentication() + and the SDK Wrapper got the certificate from the service. + + Accept (WorkflowController.accept()) the rights to continue with the workflow or completely + abort the workflow with WorkflowController.cancelWorkflow(). + + It is also possible to change the optional rights via WorkflowController.setAccessRights(). + + - Parameter error: Optional error message if the call to WorkflowController.setAccessRights() failed. + - Parameter accessRights: Requested access rights. + */ + func onAccessRights(error: String?, accessRights: AccessRights?) + + /** + Provides information about the supported API level of the employed AusweisApp2 + + Response to a call to WorkflowController.getApiLevel() and WorkflowController.setApiLevel(). + + - Parameter error: Optional error message if WorkflowController.setApiLevel() failed. + - Parameter apiLevel: Contains information about the supported and employed API level. + */ + func onApiLevel(error: String?, apiLevel: ApiLevel?) + /** + Indicates that the authentication workflow is completed. + + The authResult will contain a refresh url or in case of an error a communication error address. + You can check the state of the authentication, by looking for the AuthResult.error() field, null on success. + + - Parameter authResult: Result of the authentication + */ + func onAuthenticationCompleted(authResult: AuthResult) + + /** + An authentication has been started via WorkflowController.startAuthentication(). + + The next callback should be onAccessRights() or onAuthenticationCompleted() if the authentication immediately results + in an error. + */ + func onAuthenticationStarted() + + /** + An authentication could not be started. + This is different from an authentication that was started but failed during the process. + + - Parameter error: Error message about why the authentication could not be started. + */ + func onAuthenticationStartFailed(error: String) + + /** + Called if the sent command is not allowed within the current workflow + + - Parameter error: Error message which SDK command failed. + */ + func onBadState(error: String) + + /** + Provides information about the used certificate. + + Response of a call to WorkflowController.getCertificate(). + + - Parameter certificateDescription: Requested certificate. + */ + func onCertificate(certificateDescription: CertificateDescription) + + /** + Indicates that the pin change workflow is completed. + + - Parameter changePinResult: Result of the pin change + */ + func onChangePinCompleted(changePinResult: ChangePinResult?) + + /** + A pin change has been started via WorkflowController.startChangePin(). + */ + func onChangePinStarted() + + /** + Indicates that a CAN is required to continue workflow. + + A CAN is needed to unlock the id card, provide it with WorkflowController.setCan(). + + - Parameter error: Optional error message if the last call to WorkflowController.setCan() failed. + - Parameter reader: Information about the used card and card reader. + */ + func onEnterCan(error: String?, reader: Reader) + + /** + Indicates that a new PIN is required to continue the workflow. + + A new PIN is needed fin response to a pin change, provide it with WorkflowController.setNewPin(). + + - Parameter error: Optional error message if the last call to WorkflowController.setNewPin() failed. + - Parameter reader: Information about the used card and card reader. + */ + func onEnterNewPin(error: String?, reader: Reader) + + /** + Indicates that a PIN is required to continue the workflow. + + A PIN is needed to unlock the id card, provide it with WorkflowController.setPin(). + + - Parameter error: Optional error message if the last call to WorkflowController.setPin() failed. + - Parameter reader: Information about the used card and card reader. + */ + func onEnterPin(error: String?, reader: Reader) + + /** + Indicates that a PUK is required to continue the workflow. + + A PUK is needed to unlock the id card, provide it with WorkflowController.setPuk(). + + - Parameter error: Optional error message if the last call to WorkflowController.setPuk() failed. + - Parameter reader: Information about the used card and card reader. + */ + func onEnterPuk(error: String?, reader: Reader) + + /** + Provides information about the AusweisApp2 that is used in the SDK Wrapper. + + Response to a call to WorkflowController.getInfo(). + + - Parameter versionInfo: Holds information about the currently utilized AusweisApp2. + */ + func onInfo(versionInfo: VersionInfo) + + /** + Indicates that the workflow now requires a card to continue. + + If your application receives this message it should show a hint to the user. + After the user inserted a card the workflow will automatically continue, unless the eID functionality is disabled. + In this case, the workflow will be paused until another card is inserted. + If the user already inserted a card this function will not be called at all. + + - Parameter error: Optional detailed error message if the previous call to WorkflowController.setCard() failed. + */ + func onInsertCard(error: String?) + + /** + Called if an error within the AusweisApp2 SDK occurred. Please report this as it indicates a bug. + + - Parameter error: Information about the error. + */ + func onInternalError(error: String) + + /** + A specific reader was recognized or has vanished. Also called as a response to WorkflowController.getReader(). + + - Parameter reader: Recognized or vanished reader, might be nil if an unknown reader was requested + in getReader(). + */ + func onReader(reader: Reader?) + + /** + Called as a reponse to WorkflowController.getReaderList(). + + - Parameter readers: Optional list of present readers (if any). + */ + func onReaderList(readers: [Reader]?) + + /** + WorkflowController has successfully been initialized. + */ + func onStarted() + + /** + Provides information about the current workflow and state. This callback indicates if a + workflow is in progress or the workflow is paused. This can occur if the AusweisApp2 needs + additional data like ACCESS_RIGHTS or INSERT_CARD. + + - Parameter workflowProgress: Holds information about the current workflow progress. + */ + func onStatus(workflowProgress: WorkflowProgress) + + /** + Indicates that an error within the SDK Wrapper has occurred. + + This might be called if there was an error in the workflow. + + - Parameter error: Contains information about the error. + */ + func onWrapperError(error: WrapperError) + + // swiftformat:sort:end +} diff --git a/card/core/WorkflowController.swift b/card/core/WorkflowController.swift new file mode 100644 index 0000000..9b8a80b --- /dev/null +++ b/card/core/WorkflowController.swift @@ -0,0 +1,564 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +// swiftlint:disable file_length + +struct WeakCallbackRef: Equatable { + static func == (lhs: WeakCallbackRef, rhs: WeakCallbackRef) -> Bool { + if lhs.value == nil && rhs.value == nil { + return true + } + + if lhs.value == nil && rhs.value != nil { + return false + } + + if lhs.value != nil && rhs.value == nil { + return false + } + + return lhs.value! === rhs.value! + } + + private(set) weak var value: WorkflowCallbacks? + + init(_ value: WorkflowCallbacks) { + self.value = value + } +} + +/** + WorkflowController is used to control the authentication and pin change workflow + */ +public class WorkflowController { + public static let PinLength = 6 + public static let TransportPinLength = 5 + public static let PukLength = 10 + public static let CanLength = 6 + + private var sdkConnection: SdkConnection + var workflowCallbacks = [WeakCallbackRef]() + + init(withConnection sdkConnection: SdkConnection) { + self.sdkConnection = sdkConnection + + self.sdkConnection.onConnected = { [weak self] in + self?.callback { $0.onStarted() } + } + self.sdkConnection.onMessageReceived = { [weak self] message in + self?.handleMessage(message: message) + } + } + + deinit { + self.sdkConnection.onConnected = nil + self.sdkConnection.onMessageReceived = nil + } + + /** + Register callbacks with controller. + + - Parameter callbacks: Callbacks to register. + */ + public func registerCallbacks(_ callbacks: WorkflowCallbacks) { + let weakRef = WeakCallbackRef(callbacks) + if workflowCallbacks.contains(where: { $0 == weakRef }) { + print("\(callbacks) already registered") + return + } + workflowCallbacks.append(weakRef) + } + + /** + Unregister callbacks from controller. + + - Parameter callbacks: Callbacks to unregister. + */ + public func unregisterCallbacks(_ callbacks: WorkflowCallbacks) { + let weakRef = WeakCallbackRef(callbacks) + workflowCallbacks.removeAll(where: { $0 == weakRef || $0.value == nil }) + } + + /** + Indicates that the WorkflowController is ready to be used. + When the WorkflowController is not in started state, other api calls will fail. + */ + public var isStarted: Bool { + sdkConnection.isStarted + } + + /** + Initialize the WorkflowController. + + Before it is possible to use the WorkflowController it needs to be initialized. + Make sure to call this function and wait for the WorkflowCallbacks.onStarted callback before using it. + */ + public func start() { + guard !isStarted else { + print("WorkflowController already started") + return + } + + sdkConnection.start() + } + + /** + Stop the WorkflowController. + + When you no longer need the WorkflowController make sure to stop it to free up some resources. + */ + public func stop() { + guard isStarted else { + print("WorkflowController not started") + return + } + + sdkConnection.stop() + } + + // swiftformat:sort:begin + + /** + Accept the current state. + + If the SDK Wrapper calls WorkflowCallbacks.onAccessRights() the user needs to accept or deny them. + The workflow is paused until your application sends this command to accept the requested information. + If the user does not accept the requested information your application needs to call [cancelWorkflow] + to abort the whole workflow. + + This command is allowed only if the SDK Wrapper asked for access rights via WorkflowCallbacks.onAccessRights(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + Note: This accepts the requested access rights as well as the provider's certificate since it is not possible to + accept one without the other. + */ + public func accept() { + send(command: Accept()) + } + + /** + Cancel the running workflow. + + If your application sends this command the SDK Wrapper will cancel the workflow. + You can send this command in any state of a running workflow to abort it. + */ + public func cancel() { + send(command: Cancel()) + } + + /** + Returns information about the requested access rights. + + This command is allowed only if the SDK Wrapper called WorkflowController.onAccessRights() beforehand. + */ + public func getAccessRights() { + send(command: GetAccessRights()) + } + + /** + Returns information about the available and current API level. + + The SDK Wrapper will call WorkflowCallbacks.onApiLevel() as an answer. + */ + public func getApiLevel() { + send(command: GetApiLevel()) + } + + /** + Request the certificate of current authentication. + + The SDK Wrapper will call WorkflowCallbacks.onCertificate() as an answer. + */ + public func getCertificate() { + send(command: GetCertificate()) + } + + /** + Provides information about the utilized AusweisApp2. + + The SDK Wrapper will call WorkflowCallbacks.onInfo() as an answer. + */ + public func getInfo() { + send(command: GetInfo()) + } + + /** + Returns information about the requested reader. + + If you explicitly want to ask for information of a known reader name you can request it with this command. + + The SDK Wrapper will call WorkflowCallbacks.onReader() as an answer. + + - Parameter name: Name of the reader. + */ + public func getReader(name: String) { + send(command: GetReader(name: name)) + } + + /** + Returns information about all connected readers. + + If you explicitly want to ask for information of all connected readers you can request it with this command. + + The SDK Wrapper will call WorkflowCallbacks.onReaderList() as an answer. + */ + public func getReaderList() { + send(command: GetReaderList()) + } + + /** + Request information about the current workflow and state of SDK. + + The SDK Wrapper will call WorkflowCallbacks.onStatus() as an answer. + */ + public func getStatus() { + send(command: GetStatus()) + } + + /** + Closes the iOS NFC dialog to allow user input. + + This command is only permitted if a PIN/CAN/PUK is requested within a workflow. + */ + public func interrupt() { + send(command: Interrupt()) + } + + /** + Set optional access rights + + If the SDK Wrapper asks for specific access rights in WorkflowCallbacks.onAccessRights(), + you can modify the requested optional rights by setting a list of accepted optional rights here. + When the command is successful you get a callback to WorkflowCallbacks.onAccessRights() + with the updated access rights. + + List of possible access rights are listed in AccessRight. + + This command is allowed only if the SDK Wrapper asked for access rights via WorkflowCallbacks.onAccessRights(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter optionalAccessRights: List of enabled optional access rights. If the list is empty all + optional access rights are disabled. + */ + public func setAccessRights(_ optionalAccessRights: [AccessRight]) { + send(command: SetAccessRights(chat: optionalAccessRights.map { $0.rawValue })) + } + + /** + Set supported API level of your application. + + If you initially develop your application against the SDK Wrapper you should check + the highest supported level with getApiLevel() and set this value with this command + when you connect to the SDK Wrapper. + This will set the SDK Wrapper to act with the defined level even if a newer level is available. + + - Parameter level: Supported API level of your app. + */ + public func setApiLevel(level: Int) { + send(command: SetApiLevel(level: level)) + } + + /** + Set CAN of inserted card. + + If the SDK Wrapper calls WorkflowCallbacks.onEnterCan() you need to call this function to unblock the last retry of + setPin(). + + The CAN is required to enable the last attempt of PIN input if the retryCounter is 1. + The workflow continues automatically with the correct CAN and the SDK Wrapper will call + WorkflowCallbacks.onEnterPin(). + Despite the correct CAN being entered, the retryCounter remains at 1. + The CAN is also required, if the authentication terminal has an approved “CAN-allowed right”. + This allows the workflow to continue without an additional PIN. + + If your application provides an invalid CAN the SDK Wrapper will call WorkflowCallbacks.onEnterCan() again. + + This command is allowed only if the SDK Wrapper asked for a puk via WorkflowCallbacks.onEnterCan(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter can: The card access number (CAN) of the card. Must only contain 6 digits. + Must be nil if the current reader has a keypad. + */ + public func setCan(_ can: String?) { + send(command: SetCan(value: can)) + } + + /** + Insert “virtual” card. + + - Parameter name: Name of reader of which the Card shall be used. + - Parameter simulator: Optional specific Filesystem data for Simulator reader. + */ + public func setCard(name: String, simulator: Simulator? = nil) { + send(command: SetCard(name: name, simulator: simulator)) + } + + /** + Set new PIN for inserted card. + + If the SDK Wrapper calls WorkflowCallbacks.onEnterNewPin() you need to call this function to provide a new pin. + + This command is allowed only if the SDK Wrapper asked for a new pin via WorkflowCallbacks.onEnterNewPin(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter newPin: The new personal identification number (PIN) of the card. Must only contain 6 digits. + Must be nil if the current reader has a keypad. + */ + public func setNewPin(_ newPin: String?) { + send(command: SetNewPin(value: newPin)) + } + + /** + Set PIN of inserted card. + + If the SDK Wrapper calls WorkflowCallbacks.onEnterPin() you need to call this function to unblock + the card with the PIN. + + If your application provides an invalid PIN the SDK Wrapper will call WorkflowCallbacks.onEnterPin() + again with a decreased retryCounter. + + If the value of retryCounter is 1 the SDK Wrapper will initially call WorkflowCallbacks.onEnterCan(). + Once your application provides a correct CAN the SDK Wrapper will call WorkflowCallbacks.onEnterPin() + again with a retryCounter of 1. + If the value of retryCounter is 0 the SDK Wrapper will initially call WorkflowCallbacks.onEnterPuk(). + Once your application provides a correct PUK the SDK Wrapper will call WorkflowCallbacks.onEnterPin() + again with a retryCounter of 3. + + This command is allowed only if the SDK Wrapper asked for a pin via WorkflowCallbacks.onEnterPin(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter pin: The personal identification number (PIN) of the card. Must contain 5 (Transport PIN) or 6 digits. + Must be nil if the current reader has a keypad. + */ + public func setPin(_ pin: String?) { + send(command: SetPin(value: pin)) + } + + /** + Set PUK of inserted card. + + If the SDK Wrapper calls WorkflowCallbacks.onEnterPuk() you need to call this function to unblock setPin(). + + The workflow will automatically continue if the PUK was correct and the SDK Wrapper will call + WorkflowCallbacks.onEnterPin(). + If the correct PUK is entered the retryCounter will be set to 3. + + If your application provides an invalid PUK the SDK Wrapper will call WorkflowCallbacks.onEnterPuk() again. + + If the SDK Wrapper calls WorkflowCallbacks.onEnterPuk() with Card.inoperative set true it is not possible to unblock + the PIN. + You will have to show a message to the user that the card is inoperative and the user should + contact the authority responsible for issuing the identification card to unblock the PIN. + + This command is allowed only if the SDK Wrapper asked for a puk via WorkflowCallbacks.onEnterPuk(). + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter puk: The personal unblocking key (PUK) of the card. Must only contain 10 digits. + Must be nil if the current reader has a keypad. + */ + public func setPuk(_ puk: String?) { + send(command: SetPuk(value: puk)) + } + + /** + Starts an authentication workflow. + + The WorkflowController will call WorkflowCallbacks.onAuthenticationStarted, + when the authentication is started. If the authentication could not be started, + you will get a callback to WorkflowCallbacks.onAuthenticationStartFailed(). + + After calling this method, the expected minimal workflow is: + WorkflowCallbacks.onAuthenticationStarted() is called. + When WorkflowCallbacks.onAccessRights() is called, accept it via accept(). + WorkflowCallbacks.onInsertCard() is called, when the user has not yet placed the phone on the card. + When WorkflowCallbacks.onEnterPin() is called, provide the pin via setPin(). + When the authentication workflow is finished WorkflowCallbacks.onAuthenticationCompleted() is called. + + This command is allowed only if the SDK Wrapper has no running workflow. + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter withTcTokenUrl: URL to the TcToken. + - Parameter withDeveloperMode: Enable "Developer Mode" for test cards and disable some + security checks according to BSI TR-03124-1. + - Parameter userInfoMessages: Optional info messages to be display in the NFC dialog. + - Parameter withStatusMsgEnabled: True to enable automatic STATUS messages, which are + delivered by callbacks to WorkflowCallbacks.onStatus(). + */ + public func startAuthentication( + withTcTokenUrl tcTokenUrl: URL, + withDeveloperMode developerMode: Bool = false, + withUserInfoMessages userInfoMessages: AA2UserInfoMessages? = nil, + withStatusMsgEnabled status: Bool = true + ) { + send(command: RunAuth(tcTokenURL: tcTokenUrl.absoluteString, + developerMode: developerMode, + messages: userInfoMessages, + status: status)) + } + + /** + Start a pin change workflow. + + The WorkflowController will call WorkflowCallbacks.onChangePinStarted(), + when the pin change is started. + + After calling this method, the expected minimal workflow is: + WorkflowCallbacks.onChangePinStarted] is called. + WorkflowCallbacks.onInsertCard() is called, when the user has not yet placed the phone on the card. + When WorkflowCallbacks.onEnterPin() is called, provide the pin via setPin(). + When WorkflowCallbacks.onEnterNewPin() is called, provide the new pin via setNewPin(). + When the pin workflow is finished, WorkflowCallbacks.onChangePinCompleted() is called. + + This command is allowed only if the SDK Wrapper has no running workflow. + Otherwise you will get a callback to WorkflowCallbacks.onBadState(). + + - Parameter withStatusMsgEnabled: True to enable automatic STATUS messages, which are + delivered by callbacks to WorkflowCallbacks.onAuthenticationCompleted() + */ + public func startChangePin( + withUserInfoMessages userInfoMessages: AA2UserInfoMessages? = nil, + withStatusMsgEnabled status: Bool = true + ) { + send(command: RunChangePin(messages: userInfoMessages, status: status)) + } + + // swiftformat:sort:end + + private func send(command: T) { + guard isStarted else { + let error = WrapperError(msg: command.cmd, error: "AusweisApp2 SDK Wrapper not started") + callback { $0.onWrapperError(error: error) } + return + } + + DispatchQueue.global(qos: .userInitiated).async { + self.sdkConnection.send(command: command) + } + } + + private func callback(callback: @escaping (WorkflowCallbacks) -> Void) { + workflowCallbacks.removeAll(where: { $0.value == nil }) + + for cbRef in workflowCallbacks { + if let callbacks = cbRef.value { + DispatchQueue.main.async { callback(callbacks) } + } + } + } + + // swiftlint:disable cyclomatic_complexity function_body_length + private func handleMessage(message: AA2Message) { + switch message.msg { + case AA2Messages.MsgAuth: + if let error = message.error { + callback { $0.onAuthenticationStartFailed(error: error) } + } else if let authResult = message.getAuthResult() { + callback { $0.onAuthenticationCompleted(authResult: authResult) } + } else { + callback { $0.onAuthenticationStarted() } + } + + case AA2Messages.MsgAccessRights: + callback { $0.onAccessRights(error: message.error, accessRights: message.getAccessRights()) } + + case AA2Messages.MsgBadState: + let errorMessage = message.error ?? "Unknown bad state" + callback { $0.onBadState(error: errorMessage) } + + case AA2Messages.MsgChangePin: + if let success = message.success { + let result = ChangePinResult(success: success) + callback { $0.onChangePinCompleted(changePinResult: result) } + } else { + callback { $0.onChangePinStarted() } + } + + case AA2Messages.MsgEnterPin: + if let reader = message.getReader() { + callback { $0.onEnterPin(error: message.error, reader: reader) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing reader object") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgEnterNewPin: + if let reader = message.getReader() { + callback { $0.onEnterNewPin(error: message.error, reader: reader) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing reader object") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgEnterPuk: + if let reader = message.getReader() { + callback { $0.onEnterPuk(error: message.error, reader: reader) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing reader object") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgEnterCan: + if let reader = message.getReader() { + callback { $0.onEnterCan(error: message.error, reader: reader) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing reader object") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgInsertCard: + callback { $0.onInsertCard(error: message.error) } + + case AA2Messages.MsgCertificate: + if let certificateDescription = message.getCertificateDescription() { + callback { $0.onCertificate(certificateDescription: certificateDescription) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing or invalid certificateDescription") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgReader: + callback { $0.onReader(reader: message.getReader()) } + + case AA2Messages.MsgReaderList: + callback { $0.onReaderList(readers: message.getReaders()) } + + case AA2Messages.MsgInvalid, AA2Messages.MsgUnknowCommand: + let error = WrapperError(msg: message.msg, error: message.error ?? "Unknown SDK Wrapper error") + callback { $0.onWrapperError(error: error) } + + case AA2Messages.MsgInternalError: + let errorMessage = message.error ?? "Unknown internal error" + callback { $0.onInternalError(error: errorMessage) } + + case AA2Messages.MsgStatus: + let workflowProgress = WorkflowProgress(workflow: message.workflow, + progress: message.progress, + state: message.state) + callback { $0.onStatus(workflowProgress: workflowProgress) } + + case AA2Messages.MsgInfo: + if let info = message.versionInfo { + let versionInfo = VersionInfo(info: info) + + callback { $0.onInfo(versionInfo: versionInfo) } + } else { + let error = WrapperError(msg: message.msg, error: "Missing VersionInfo in message") + callback { $0.onWrapperError(error: error) } + } + + case AA2Messages.MsgApiLevel: + if let current = message.current { + let apiLevel = ApiLevel(available: message.available, current: current) + callback { $0.onApiLevel(error: message.error, apiLevel: apiLevel) } + } else { + callback { $0.onApiLevel(error: message.error, apiLevel: nil) } + } + + default: + print("Received unknown message \(message.msg)") + } + } +} diff --git a/card/core/WorkflowData.swift b/card/core/WorkflowData.swift new file mode 100644 index 0000000..06c33d3 --- /dev/null +++ b/card/core/WorkflowData.swift @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +/// Detailed description of the certificate. +public struct CertificateDescription { + /// Name of the certificate issuer. + public let issuerName: String + + /// URL of the certificate issuer. + public let issuerUrl: URL? + + /// Parsed purpose of the terms of usage. + public let purpose: String + + /// Name of the certificate subject. + public let subjectName: String + + /// URL of the certificate subject. + public let subjectUrl: URL? + + /// Raw certificate information about the terms of usage. + public let termsOfUsage: String + + /// Certificate validity + public let validity: CertificateValidity +} + +/// Validity dates of the certificate. +public struct CertificateValidity { + /// Certificate is valid since this date. + public let effectiveDate: Date + + /// Certificate is invalid after this date. + public let expirationDate: Date +} + +/// Access rights requested by the provider. +public struct AccessRights { + /// These rights are mandatory and cannot be disabled. + public let requiredRights: [AccessRight] + + /// These rights are optional and can be enabled or disabled + public let optionalRights: [AccessRight] + + /// Indicates the enabled access rights of optional and required. + public let effectiveRights: [AccessRight] + + /// Optional transaction information. + public let transactionInfo: String? + + /// Optional auxiliary data of the provider. + public let auxiliaryData: AuxiliaryData? +} + +/// Auxiliary data of the provider. +public struct AuxiliaryData { + /// Optional required date of birth for AgeVerification. + public let ageVerificationDate: Date? + + /// Optional required age for AgeVerification. + /// It is calculated by the SDK Wrapper on the basis of ageVerificationDate and current date. + public let requiredAge: Int? + + /// Optional validity date. + public let validityDate: Date? + + /// Optional id of community. + public let communityId: String? +} + +/// Provides information about a reader. +public struct Reader { + /// Identifier of card reader. + public let name: String + + /// Indicates whether a card can be inserted via setCard() + public let insertable: Bool + + /// Indicates whether a card reader is connected or disconnected. + public let attached: Bool + + /// Indicates whether a card reader has a keypad. The parameter is only shown when a reader is attached. + public let keypad: Bool + + /// Provides information about inserted card, otherwise nil. + public let card: Card? + + init(name: String, insertable: Bool, attached: Bool, keypad: Bool, card: Card?) { + self.name = name + self.insertable = insertable + self.attached = attached + self.keypad = keypad + self.card = card + } + + init(reader: AA2Reader?) { + name = reader?.name ?? "" + insertable = reader?.insertable ?? false + attached = reader?.attached ?? false + keypad = reader?.keypad ?? false + card = reader?.card != nil ? Card(card: reader?.card) : nil + } +} + +/// Provides information about inserted card. +public struct Card { + /// True if PUK is inoperative and cannot unblock PIN otherwise false. + /// This can be recognized if user enters a correct PUK only. + /// It is not possible to read this data before a user tries to unblock the PIN. + public let deactivated: Bool + + /// True if eID functionality is deactivated otherwise false. + public let inoperative: Bool + + /// Count of possible retries for the PIN. If you enter a PIN it will be decreased if PIN was incorrect. + public let pinRetryCounter: Int + + init(card: AA2Card?) { + deactivated = card?.deactivated ?? true + inoperative = card?.inoperative ?? true + pinRetryCounter = card?.retryCounter ?? 0 + } +} + +/// Final result of an authentication. +public struct AuthResult { + /// Refresh url or communication error address (which is optional). + public let url: URL? + + /// Contains information about the result of the authentication. + public let result: AuthResultData? +} + +/// Final result of a PIN change. +public struct ChangePinResult { + // False if an error occured or the PIN change was aborted. + public let success: Bool +} + +/// Information about an authentication. +public struct AuthResultData { + /// Major error code. + public let major: String + + /// Minor error code. + public let minor: String? + + /// Language of description and message. Language “en” is supported only at the moment. + public let language: String? + + /// Description of the error message. + public let description: String? + + /// The error message. + public let message: String? + + /// Unique error code. + public let reason: String? +} + +/// Provides information about an error. +public struct WrapperError { + /// Message type in which the error occurred. + public let msg: String + + /// Error message. + public let error: String +} + +/// Provides information about the workflow status +public struct WorkflowProgress { + /// Type of the current workflow. If there is no workflow in progress this will be null. + public let workflow: WorkflowProgressType? + + /// Percentage of workflow progress. If there is no workflow in progress this will be null. + public let progress: Int? + + /// Name of the current state if paused. If there is no workflow in progress or the workflow + /// is not paused this will be null. + public let state: String? + + public init() { + workflow = nil + progress = nil + state = nil + } + + init(workflow: String?, progress: Int?, state: String?) { + self.workflow = WorkflowProgressType(rawValue: workflow ?? "") + self.progress = progress + self.state = state + } +} + +/// Provides information about the underlying AusweisApp2 +public struct VersionInfo { + /// Application name. + public let name: String + + /// Title of implementation. + public let implementationTitle: String + + /// Vendor of implementation. + public let implementationVendor: String + + /// Version of implementation. + public let implementationVersion: String + + /// Title of specification. + public let specificationTitle: String + + /// Vendor of specification. + public let specificationVendor: String + + /// Version of specification. + public let specificationVersion: String + + init(info: AA2VersionInfo) { + name = info.name + implementationTitle = info.implementationTitle + implementationVendor = info.implementationVendor + implementationVersion = info.implementationVersion + specificationTitle = info.specificationTitle + specificationVendor = info.specificationVendor + specificationVersion = info.specificationVersion + } +} + +/// Provides information about the API Level of the underlying AusweisApp2 +public struct ApiLevel { + /// List of available API level, nil in a response to WorkflowController.setApiLevel(). + public let available: [Int]? + + /// Currently set API Level. + public let current: Int +} + +// swiftlint:disable identifier_name +/// List of all available access rights a provider might request. +public enum AccessRight: String { + case Address, + BirthName, + FamilyName, + GivenNames, + PlaceOfBirth, + DateOfBirth, + DoctoralDegree, + ArtisticName, + Pseudonym, + ValidUntil, + Nationality, + IssuingCountry, + DocumentType, + ResidencePermitI, + ResidencePermitII, + CommunityID, + AddressVerification, + AgeVerification, + WriteAddress, + WriteCommunityID, + WriteResidencePermitI, + WriteResidencePermitII, + CanAllowed, + PinManagement +} + +/// List of all types of WorkflowProgess +public enum WorkflowProgressType: String { + case AUTHENTICATION = "AUTH" + case CHANGE_PIN +} + +// swiftlint:disable opening_brace +/// Messages for the NFC system dialog +public struct AA2UserInfoMessages: Encodable { + public init(sessionStarted: String? = "", + sessionFailed: String? = "", + sessionSucceeded: String? = "", + sessionInProgress: String? = "") + { + self.sessionStarted = sessionStarted + self.sessionFailed = sessionFailed + self.sessionSucceeded = sessionSucceeded + self.sessionInProgress = sessionInProgress + } + + /// Shown if scanning is started + let sessionStarted: String? + /// Shown if communication was stopped with an error. + let sessionFailed: String? + /// Shown if communication was stopped successfully. + let sessionSucceeded: String? + /// Shown if communication is in progress. This message will be appended with current percentage level. + let sessionInProgress: String? +} + +/// Optional definition of files for the Simulator reader +public struct Simulator: Encodable { + /// List of SimulatorFile definitions + let files: [SimulatorFile] + + public init(withFiles: [SimulatorFile]) { + files = withFiles + } +} + +/// Filesystem for Simulator reader +/// The content of the filesystem can be provided as a JSON array of objects. +/// The fileId and shortFileId are specified on the corresponding technical guideline +/// of the BSI and ISO. The content is an ASN.1 structure in DER encoding. +/// All fields are hex encoded. +public struct SimulatorFile: Encodable { + let fileId: String + let shortFileId: String + let content: String + public init(withFileId: String, withShortFileId: String, withContent: String) { + fileId = withFileId + shortFileId = withShortFileId + content = withContent + } +} diff --git a/card/core/WorkflowDataExtensions.swift b/card/core/WorkflowDataExtensions.swift new file mode 100644 index 0000000..c5a57ac --- /dev/null +++ b/card/core/WorkflowDataExtensions.swift @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +public extension AuthResult { + var hasError: Bool { + result?.major.contains("resultmajor#error") ?? false + } +} + +public extension AuthResultData { + var isCancellationByUser: Bool { + minor?.contains("cancellationByUser") ?? false + } +} diff --git a/common/StringExtensions.swift b/common/StringExtensions.swift new file mode 100644 index 0000000..ca294ca --- /dev/null +++ b/common/StringExtensions.swift @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +extension String { + public var isNumber: Bool { + return !isEmpty && rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil + } + + func parseDate(format: String) -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = format + return dateFormatter.date(from: self) + } +} diff --git a/common/URLExtensions.swift b/common/URLExtensions.swift new file mode 100644 index 0000000..e2e39ae --- /dev/null +++ b/common/URLExtensions.swift @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2020-2023 Governikus GmbH & Co. KG, Germany + */ + +import Foundation + +public extension URL { + var isValidHttpsURL: Bool { + guard let scheme = scheme, scheme == "https" else { return false } + + let string = absoluteString + + let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) + if let match = detector?.firstMatch( + in: string, + options: [], + range: NSRange(location: 0, length: string.utf16.count) + ) { + return match.range.length == string.utf16.count + } + + return false + } +}