diff --git a/Package.swift b/Package.swift index 9c7b305..b9f85e3 100644 --- a/Package.swift +++ b/Package.swift @@ -13,7 +13,6 @@ let package = Package( targets: ["MdocDataTransfer18013"]), ], dependencies: [ - //.package(path: "../MdocSecurity18013"), .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-ios-iso18013-security.git", branch: "develop"), .package(url: "https://github.com/apple/swift-log.git", branch: "main"), .package(url: "https://github.com/valpackett/SwiftCBOR.git", branch: "master"), diff --git a/Sources/MdocDataTransfer18013/BLETransfer/MdocGATTServer.swift b/Sources/MdocDataTransfer18013/BLETransfer/MdocGATTServer.swift index d1d8648..9c8097c 100644 --- a/Sources/MdocDataTransfer18013/BLETransfer/MdocGATTServer.swift +++ b/Sources/MdocDataTransfer18013/BLETransfer/MdocGATTServer.swift @@ -22,11 +22,12 @@ import CoreBluetooth import UIKit #endif import Logging +import ASN1Decoder import MdocDataModel18013 import MdocSecurity18013 /// BLE Gatt server implementation of mdoc transfer manager -public class MdocGattServer: ObservableObject, MdocTransferManager { +public class MdocGattServer: ObservableObject { var peripheralManager: CBPeripheralManager! var bleDelegate: Delegate! var remoteCentral: CBCentral! @@ -34,27 +35,27 @@ public class MdocGattServer: ObservableObject, MdocTransferManager { var server2ClientCharacteristic: CBMutableCharacteristic! public var deviceEngagement: DeviceEngagement? public var deviceRequest: DeviceRequest? - public var deviceResponseToSend: DeviceResponse? - public var validRequestItems: RequestItems? - public var errorRequestItems: RequestItems? public var sessionEncryption: SessionEncryption? public var docs: [DeviceResponse]! public var iaca: [SecCertificate]! public var devicePrivateKey: CoseKeyPrivate! public var readerName: String? - @Published public var qrCodeImageData: Data? + public var qrCodeImageData: Data? public weak var delegate: (any MdocOfflineDelegate)? - @Published public var advertising: Bool = false - @Published public var error: Error? = nil { willSet { handleErrorSet(newValue) }} - @Published public var status: TransferStatus = .initializing { willSet { handleStatusChange(newValue) }} - public var requireUserAccept = false + public var advertising: Bool = false + public var error: Error? = nil { willSet { handleErrorSet(newValue) }} + public var status: TransferStatus = .initializing { willSet { handleStatusChange(newValue) } } var readBuffer = Data() var sendBuffer = [Data]() var numBlocks: Int = 0 var subscribeCount: Int = 0 - public init(status: TransferStatus = .initializing) { - self.status = status + public init(parameters: [String: Any]) throws { + guard let (docs, devicePrivateKey, iaca) = MdocHelpers.initializeData(parameters: parameters) else { + throw Self.makeError(code: .documents_not_provided) + } + self.docs = docs; self.devicePrivateKey = devicePrivateKey; self.iaca = iaca + status = .initialized; handleStatusChange(status) } @objc(CBPeripheralManagerDelegate) @@ -195,7 +196,6 @@ public class MdocGattServer: ObservableObject, MdocTransferManager { peripheralManager.stopAdvertising() deviceRequest = decodeRequestAndInformUser(requestData: readBuffer, devicePrivateKey: devicePrivateKey, readerKeyRawData: nil, handOver: BleTransferMode.QRHandover, handler: userSelected) if deviceRequest == nil { error = Self.makeError(code: .requestDecodeError) } - if requireUserAccept == false /*|| _isDebugAssertConfiguration() */ { userSelected(true, nil) } } else if newValue == .initialized { bleDelegate = Delegate(server: self) @@ -218,12 +218,12 @@ public class MdocGattServer: ObservableObject, MdocTransferManager { if !b { error = Self.makeError(code: .userRejected) } if let items { do { - try getDeviceResponseToSend(deviceRequest!, selectedItems: items, eReaderKey: sessionEncryption!.sessionKeys.publicKey, devicePrivateKey: devicePrivateKey) + guard let (docToSend, _, _) = try MdocHelpers.getDeviceResponseToSend(deviceRequest: deviceRequest!, deviceResponses: docs, selectedItems: items, sessionEncryption: sessionEncryption, eReaderKey: sessionEncryption!.sessionKeys.publicKey, devicePrivateKey: devicePrivateKey) else { error = Self.makeError(code: .noDocumentToReturn); return } + guard let bytes = getSessionDataToSend(docToSend: docToSend) else { error = Self.makeError(code: .noDocumentToReturn); return } + prepareDataToSend(bytes) + DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { self.sendDataWithUpdates() } } catch { self.error = error } } - guard let bytes = getSessionDataToSend(deviceRequest!, eReaderKey: sessionEncryption!.sessionKeys.publicKey) else { error = Self.makeError(code: .noDocumentToReturn); return } - prepareDataToSend(bytes) - DispatchQueue.main.asyncAfter(deadline: .now()+0.2) { self.sendDataWithUpdates() } } func handleErrorSet(_ newValue: Error?) { @@ -235,12 +235,12 @@ public class MdocGattServer: ObservableObject, MdocTransferManager { func prepareDataToSend(_ msg: Data) { let mbs = min(511, remoteCentral.maximumUpdateValueLength-1) - numBlocks = Helpers.CountNumBlocks(dataLength: msg.count, maxBlockSize: mbs) + numBlocks = MdocHelpers.CountNumBlocks(dataLength: msg.count, maxBlockSize: mbs) logger.info("Sending response of total bytes \(msg.count) in \(numBlocks) blocks and block size: \(mbs)") sendBuffer.removeAll() // send blocks for i in 0.. 0 else { status = .responseSent; logger.info("Finished sending BLE data") + stop() return } let b = peripheralManager.updateValue(sendBuffer.first!, for: server2ClientCharacteristic, onSubscribedCentrals: [remoteCentral]) if b, sendBuffer.count > 0 { sendBuffer.removeFirst(); sendDataWithUpdates() } } + + public func getSessionDataToSend(docToSend: DeviceResponse) -> Data? { + do { + guard var sessionEncryption else { logger.error("Session Encryption not initialized"); return nil } + if docToSend.documents == nil { logger.error("Could not create documents to send") } + let cborToSend = docToSend.toCBOR(options: CBOROptions()) + let clearBytesToSend = cborToSend.encode() + guard let cipherData = try sessionEncryption.encrypt(clearBytesToSend) else { return nil } + let sd = SessionData(cipher_data: status == .error ? nil : cipherData, status: status == .error ? 0 : 20) + return Data(sd.encode(options: CBOROptions())) + } catch { self.error = error} + return nil + } + + /// Decrypt the contents of a data object and return a ``DeviceRequest`` object if the data represents a valid device request. If the data does not represent a valid device request, the function returns nil. + /// - Parameters: + /// - requestData: Request data passed to the mdoc holder + /// - handler: Handler to call with the accept/reject flag + /// - devicePrivateKey: Device private key + /// - readerKeyRawData: reader key cbor data (if reader engagement is used) + /// - Returns: A ``DeviceRequest`` object + + public func decodeRequestAndInformUser(requestData: Data, devicePrivateKey: CoseKeyPrivate, readerKeyRawData: [UInt8]?, handOver: CBOR, handler: @escaping (Bool, RequestItems?) -> Void) -> DeviceRequest? { + do { + guard let seCbor = try CBOR.decode([UInt8](requestData)) else { logger.error("Request Data is not Cbor"); return nil } + guard var se = SessionEstablishment(cbor: seCbor) else { logger.error("Request Data cannot be decoded to session establisment"); return nil } + if se.eReaderKeyRawData == nil, let readerKeyRawData { se.eReaderKeyRawData = readerKeyRawData } + guard se.eReaderKey != nil else { logger.error("Reader key not available"); return nil } + let requestCipherData = se.data + guard let deviceEngagement else { logger.error("Device Engagement not initialized"); return nil } + // init session-encryption object from session establish message and device engagement, decrypt data + sessionEncryption = SessionEncryption(se: se, de: deviceEngagement, handOver: handOver) + guard var sessionEncryption else { logger.error("Session Encryption not initialized"); return nil } + guard let requestData = try sessionEncryption.decrypt(requestCipherData) else { logger.error("Request data cannot be decrypted"); return nil } + guard let deviceRequest = DeviceRequest(data: requestData) else { logger.error("Decrypted data cannot be decoded"); return nil } + guard let (_, validRequestItems, errorRequestItems) = try MdocHelpers.getDeviceResponseToSend(deviceRequest: deviceRequest, deviceResponses: docs, selectedItems: nil, sessionEncryption: sessionEncryption, eReaderKey: sessionEncryption.sessionKeys.publicKey, devicePrivateKey: devicePrivateKey) else { logger.error("Valid request items nil"); return nil } + var params: [String: Any] = [UserRequestKeys.valid_items_requested.rawValue: validRequestItems, UserRequestKeys.error_items_requested.rawValue: errorRequestItems] + if let docR = deviceRequest.docRequests.first { + let mdocAuth = MdocReaderAuthentication(transcript: sessionEncryption.transcript) + if let readerAuthRawCBOR = docR.readerAuthRawCBOR, let certData = docR.readerCertificate, let x509 = try? X509Certificate(der: certData), let issName = x509.issuerDistinguishedName, let (b,reasonFailure) = try? mdocAuth.validateReaderAuth(readerAuthCBOR: readerAuthRawCBOR, readerAuthCertificate: certData, itemsRequestRawData: docR.itemsRequestRawData!, rootCerts: iaca) { + params[UserRequestKeys.reader_certificate_issuer.rawValue] = issName + params[UserRequestKeys.reader_auth_validated.rawValue] = b + if let reasonFailure { params[UserRequestKeys.reader_certificate_validation_message.rawValue] = reasonFailure } + } + } + self.deviceRequest = deviceRequest + delegate?.didReceiveRequest(params, handleSelected: handler) + return deviceRequest + } catch { self.error = error} + return nil + } + + public static func makeError(code: ErrorCode, str: String? = nil) -> NSError { + let errorMessage = str ?? NSLocalizedString(code.description, comment: code.description) + logger.error(Logger.Message(unicodeScalarLiteral: errorMessage)) + return NSError(domain: "\(MdocGattServer.self)", code: code.rawValue, userInfo: [NSLocalizedDescriptionKey: errorMessage]) + } } diff --git a/Sources/MdocDataTransfer18013/Protocols/MdocOfflineDelegate.swift b/Sources/MdocDataTransfer18013/BLETransfer/MdocOfflineDelegate.swift similarity index 100% rename from Sources/MdocDataTransfer18013/Protocols/MdocOfflineDelegate.swift rename to Sources/MdocDataTransfer18013/BLETransfer/MdocOfflineDelegate.swift diff --git a/Sources/MdocDataTransfer18013/BLETransfer/TransferStatus.swift b/Sources/MdocDataTransfer18013/Enumerations.swift similarity index 78% rename from Sources/MdocDataTransfer18013/BLETransfer/TransferStatus.swift rename to Sources/MdocDataTransfer18013/Enumerations.swift index d39c281..4fa4d20 100644 --- a/Sources/MdocDataTransfer18013/BLETransfer/TransferStatus.swift +++ b/Sources/MdocDataTransfer18013/Enumerations.swift @@ -5,7 +5,7 @@ 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 + 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, @@ -60,3 +60,20 @@ public enum ErrorCode: Int, CustomStringConvertible { } } } + +/// String keys for the initialization dictionary +public enum InitializeKeys: String { + case document_json_data + case document_signup_response_data + case device_private_key + case trusted_certificates +} + +/// String keys for the user request dictionary +public enum UserRequestKeys: String { + case valid_items_requested + case error_items_requested + case reader_certificate_issuer + case reader_auth_validated + case reader_certificate_validation_message +} diff --git a/Sources/MdocDataTransfer18013/Helpers.swift b/Sources/MdocDataTransfer18013/Helpers.swift deleted file mode 100644 index d239ff9..0000000 --- a/Sources/MdocDataTransfer18013/Helpers.swift +++ /dev/null @@ -1,145 +0,0 @@ -/* -Copyright (c) 2023 European Commission - -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. -*/ - -// Helpers.swift -import Foundation -import CoreBluetooth -import Combine -#if canImport(UIKit) -import UIKit -#endif -import AVFoundation - -/// Helper methods -public class Helpers { - /// Returns the number of blocks that dataLength bytes of data can be split into, given a maximum block size of maxBlockSize bytes. - /// - Parameters: - /// - dataLength: Length of data to be split - /// - maxBlockSize: The maximum block size - /// - Returns: Number of blocks - public static func CountNumBlocks(dataLength: Int, maxBlockSize: Int) -> Int { - let blockSize = maxBlockSize - var numBlocks = 0 - if dataLength > maxBlockSize { - numBlocks = dataLength / blockSize; - if numBlocks * blockSize < dataLength { - numBlocks += 1 - } - } else if dataLength > 0 { - numBlocks = 1 - } - return numBlocks - } - - /// Creates a block for a given block id from a data object. The block size is limited to maxBlockSize bytes. - /// - Parameters: - /// - data: The data object to be sent - /// - blockId: The id (number) of the block to be sent - /// - maxBlockSize: The maximum block size - /// - Returns: (chunk:The data block, bEnd: True if this is the last block, false otherwise) - public static func CreateBlockCommand(data: Data, blockId: Int, maxBlockSize: Int) -> (Data, Bool) { - let start = blockId * maxBlockSize - var end = (blockId+1) * maxBlockSize - var bEnd = false - if end >= data.count { - end = data.count - bEnd = true - } - let chunk = data.subdata(in: start..Void) { - switch CBManager.authorization { - case .denied: - // "Denied, request permission from settings" - presentSettings(vc, msg: NSLocalizedString("Bluetooth access is denied", comment: "")) - case .restricted: - logger.warning("Restricted, device owner must approve") - case .allowedAlways: - // "Authorized, proceed" - DispatchQueue.main.async { action() } - case .notDetermined: - DispatchQueue.main.async { action() } - @unknown default: - logger.info("Unknown authorization status") - } - } - - /// Check if the user has given permission to access the camera. If not, ask them to go to the settings app to give permission. - /// - Parameters: - /// - vc: The view controller that will present the settings - /// - action: The action to perform - public static func checkCameraAccess(_ vc: UIViewController, action: @escaping ()->Void) { - switch AVCaptureDevice.authorizationStatus(for: .video) { - case .denied: - // "Denied, request permission from settings" - presentSettings(vc, msg: NSLocalizedString("Camera access is denied", comment: "")) - case .restricted: - logger.warning("Restricted, device owner must approve") - case .authorized: - // "Authorized, proceed" - DispatchQueue.main.async { action() } - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { success in - if success { - DispatchQueue.main.async { action() } - } else { - logger.info("Permission denied") - } - } - @unknown default: - logger.info("Unknown authorization status") - } - } - - /// Present an alert controller with a message, and two actions, one to cancel, and one to go to the settings page. - /// - Parameters: - /// - vc: The view controller that will present the settings - /// - msg: The message to show - public static func presentSettings(_ vc: UIViewController, msg: String) { - let alertController = UIAlertController(title: NSLocalizedString("error", comment: ""), message: msg, preferredStyle: .alert) - alertController.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .default)) - alertController.addAction(UIAlertAction(title: NSLocalizedString("settings", comment: ""), style: .cancel) { _ in - if let url = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(url, options: [:], completionHandler: { _ in - // Handle - }) - } - }) - vc.present(alertController, animated: true) - } - - /// Finds the top view controller in the view hierarchy of the app. It is used to present a new view controller on top of any existing view controllers. - public static func getTopViewController(base: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? { - if let nav = base as? UINavigationController { - return getTopViewController(base: nav.visibleViewController) - } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { - return getTopViewController(base: selected) - } else if let presented = base?.presentedViewController { - return getTopViewController(base: presented) - } - return base - } - - #endif -} diff --git a/Sources/MdocDataTransfer18013/MdocHelpers.swift b/Sources/MdocDataTransfer18013/MdocHelpers.swift new file mode 100644 index 0000000..006768b --- /dev/null +++ b/Sources/MdocDataTransfer18013/MdocHelpers.swift @@ -0,0 +1,232 @@ +/* +Copyright (c) 2023 European Commission + +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. +*/ + +// Helpers.swift +import Foundation +import CoreBluetooth +import Combine +import MdocDataModel18013 +import MdocSecurity18013 +#if canImport(UIKit) +import UIKit +#endif +import AVFoundation + +public typealias RequestItems = [String: [String: [String]]] + +/// Helper methods +public class MdocHelpers { + + public static func initializeData(parameters: [String: Any]) -> (docs: [DeviceResponse], devicePrivateKey: CoseKeyPrivate?, iaca: [SecCertificate]?)? { + var docs: [DeviceResponse]? + var devicePrivateKey: CoseKeyPrivate? + var iaca: [SecCertificate]? + if let d = parameters[InitializeKeys.document_json_data.rawValue] as? [Data] { + // load json sample data here + let sampleData = d.compactMap { $0.decodeJSON(type: SignUpResponse.self) } + docs = sampleData.compactMap { $0.deviceResponse } + devicePrivateKey = sampleData.compactMap { $0.devicePrivateKey }.first + } else if let drs = parameters[InitializeKeys.document_signup_response_data.rawValue] as? [DeviceResponse], let dpk = parameters[InitializeKeys.device_private_key.rawValue] as? CoseKeyPrivate { + docs = drs + devicePrivateKey = dpk + } + if let i = parameters[InitializeKeys.trusted_certificates.rawValue] as? [Data] { + iaca = i.compactMap { SecCertificateCreateWithData(nil, $0 as CFData) } + } + guard let docs else { return nil } + return (docs, devicePrivateKey, iaca) + } + + public static func getDeviceResponseToSend(deviceRequest: DeviceRequest?, deviceResponses: [DeviceResponse], selectedItems: RequestItems? = nil, sessionEncryption: SessionEncryption? = nil, eReaderKey: CoseKey? = nil, devicePrivateKey: CoseKeyPrivate? = nil) throws -> (response: DeviceResponse, validRequestItems: RequestItems, errorRequestItems: RequestItems)? { + let documents = deviceResponses.flatMap { $0.documents! } + var docFiltered = [Document](); var docErrors = [[DocType: UInt64]]() + var validReqItemsDocDict = RequestItems(); var errorReqItemsDocDict = RequestItems() + guard deviceRequest != nil || selectedItems != nil else { fatalError("Invalid call") } + let haveDeviceRequest = deviceRequest != nil + let reqDocTypes = haveDeviceRequest ? deviceRequest!.docRequests.map(\.itemsRequest.docType) : Array(selectedItems!.keys) + for reqDocType in reqDocTypes { + let docReq = deviceRequest?.docRequests.findDoc(name: reqDocType) + guard let doc = documents.findDoc(name: reqDocType) else { + docErrors.append([reqDocType: UInt64(0)]) + errorReqItemsDocDict[reqDocType] = [:] + continue + } + guard let issuerNs = doc.issuerSigned.issuerNameSpaces else { logger.error("Null issuer namespaces"); return nil } + var nsItemsToAdd = [NameSpace: [IssuerSignedItem]]() + var nsErrorsToAdd = [NameSpace : ErrorItems]() + var validReqItemsNsDict = [NameSpace: [String]]() + // for each request namespace + let reqNamespaces = haveDeviceRequest ? Array(docReq!.itemsRequest.requestNameSpaces.nameSpaces.keys) : Array(selectedItems![reqDocType]!.keys) + for reqNamespace in reqNamespaces { + let reqElementIdentifiers = haveDeviceRequest ? docReq!.itemsRequest.requestNameSpaces.nameSpaces[reqNamespace]!.elementIdentifiers : Array(selectedItems![reqDocType]![reqNamespace]!) + guard let items = issuerNs[reqNamespace] else { + nsErrorsToAdd[reqNamespace] = Dictionary(grouping: reqElementIdentifiers, by: {$0}).mapValues { _ in 0 } + continue + } + let itemsReqSet = Set(reqElementIdentifiers).subtracting(IsoMdlModel.self.moreThan2AgeOverElementIdentifiers(reqDocType, reqNamespace, SimpleAgeAttest(namespaces: issuerNs.nameSpaces), reqElementIdentifiers)) + let itemsSet = Set(items.map(\.elementIdentifier)) + var itemsToAdd = items.filter({ itemsReqSet.contains($0.elementIdentifier) }) + if let selectedItems { + let selectedNsItems = selectedItems[reqDocType]?[reqNamespace] ?? [] + itemsToAdd = itemsToAdd.filter({ selectedNsItems.contains($0.elementIdentifier) }) + } + if itemsToAdd.count > 0 { + nsItemsToAdd[reqNamespace] = itemsToAdd + validReqItemsNsDict[reqNamespace] = itemsToAdd.map(\.elementIdentifier) + } + let errorItemsSet = itemsReqSet.subtracting(itemsSet) + if errorItemsSet.count > 0 { + nsErrorsToAdd[reqNamespace] = Dictionary(grouping: errorItemsSet, by: { $0 }).mapValues { _ in 0 } + } + } // end ns for + let issuerAuthToAdd = doc.issuerSigned.issuerAuth + let issToAdd = IssuerSigned(issuerNameSpaces: IssuerNameSpaces(nameSpaces: nsItemsToAdd), issuerAuth: issuerAuthToAdd) + var devSignedToAdd: DeviceSigned? = nil + if let eReaderKey, let sessionEncryption, let devicePrivateKey { + let authKeys = CoseKeyExchange(publicKey: eReaderKey, privateKey: devicePrivateKey) + let mdocAuth = MdocAuthentication(transcript: sessionEncryption.transcript, authKeys: authKeys) + guard let devAuth = try mdocAuth.getDeviceAuthForTransfer(docType: reqDocType) else {logger.error("Cannot create device auth"); return nil } + devSignedToAdd = DeviceSigned(deviceAuth: devAuth) + } + let errors: Errors? = nsErrorsToAdd.count == 0 ? nil : Errors(errors: nsErrorsToAdd) + let docToAdd = Document(docType: reqDocType, issuerSigned: issToAdd, deviceSigned: devSignedToAdd, errors: errors) + docFiltered.append(docToAdd) + validReqItemsDocDict[reqDocType] = validReqItemsNsDict + errorReqItemsDocDict[reqDocType] = nsErrorsToAdd.mapValues { Array($0.keys) } + } // end doc for + let documentErrors: [DocumentError]? = docErrors.count == 0 ? nil : docErrors.map(DocumentError.init(docErrors:)) + let deviceResponseToSend = DeviceResponse(version: deviceResponses.first!.version, documents: docFiltered, documentErrors: documentErrors, status: 0) + return (deviceResponseToSend, validReqItemsDocDict, errorReqItemsDocDict) + } + + /// Returns the number of blocks that dataLength bytes of data can be split into, given a maximum block size of maxBlockSize bytes. + /// - Parameters: + /// - dataLength: Length of data to be split + /// - maxBlockSize: The maximum block size + /// - Returns: Number of blocks + public static func CountNumBlocks(dataLength: Int, maxBlockSize: Int) -> Int { + let blockSize = maxBlockSize + var numBlocks = 0 + if dataLength > maxBlockSize { + numBlocks = dataLength / blockSize; + if numBlocks * blockSize < dataLength { + numBlocks += 1 + } + } else if dataLength > 0 { + numBlocks = 1 + } + return numBlocks + } + + /// Creates a block for a given block id from a data object. The block size is limited to maxBlockSize bytes. + /// - Parameters: + /// - data: The data object to be sent + /// - blockId: The id (number) of the block to be sent + /// - maxBlockSize: The maximum block size + /// - Returns: (chunk:The data block, bEnd: True if this is the last block, false otherwise) + public static func CreateBlockCommand(data: Data, blockId: Int, maxBlockSize: Int) -> (Data, Bool) { + let start = blockId * maxBlockSize + var end = (blockId+1) * maxBlockSize + var bEnd = false + if end >= data.count { + end = data.count + bEnd = true + } + let chunk = data.subdata(in: start..Void) { + switch CBManager.authorization { + case .denied: + // "Denied, request permission from settings" + presentSettings(vc, msg: NSLocalizedString("Bluetooth access is denied", comment: "")) + case .restricted: + logger.warning("Restricted, device owner must approve") + case .allowedAlways: + // "Authorized, proceed" + DispatchQueue.main.async { action() } + case .notDetermined: + DispatchQueue.main.async { action() } + @unknown default: + logger.info("Unknown authorization status") + } + } + + /// Check if the user has given permission to access the camera. If not, ask them to go to the settings app to give permission. + /// - Parameters: + /// - vc: The view controller that will present the settings + /// - action: The action to perform + public static func checkCameraAccess(_ vc: UIViewController, action: @escaping ()->Void) { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .denied: + // "Denied, request permission from settings" + presentSettings(vc, msg: NSLocalizedString("Camera access is denied", comment: "")) + case .restricted: + logger.warning("Restricted, device owner must approve") + case .authorized: + // "Authorized, proceed" + DispatchQueue.main.async { action() } + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { success in + if success { + DispatchQueue.main.async { action() } + } else { + logger.info("Permission denied") + } + } + @unknown default: + logger.info("Unknown authorization status") + } + } + + /// Present an alert controller with a message, and two actions, one to cancel, and one to go to the settings page. + /// - Parameters: + /// - vc: The view controller that will present the settings + /// - msg: The message to show + public static func presentSettings(_ vc: UIViewController, msg: String) { + let alertController = UIAlertController(title: NSLocalizedString("error", comment: ""), message: msg, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .default)) + alertController.addAction(UIAlertAction(title: NSLocalizedString("settings", comment: ""), style: .cancel) { _ in + if let url = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url, options: [:], completionHandler: { _ in + // Handle + }) + } + }) + vc.present(alertController, animated: true) + } + + /// Finds the top view controller in the view hierarchy of the app. It is used to present a new view controller on top of any existing view controllers. + public static func getTopViewController(base: UIViewController? = UIApplication.shared.windows.first { $0.isKeyWindow }?.rootViewController) -> UIViewController? { + if let nav = base as? UINavigationController { + return getTopViewController(base: nav.visibleViewController) + } else if let tab = base as? UITabBarController, let selected = tab.selectedViewController { + return getTopViewController(base: selected) + } else if let presented = base?.presentedViewController { + return getTopViewController(base: presented) + } + return base + } + + #endif +} diff --git a/Sources/MdocDataTransfer18013/Protocols/MdocTransferManager.swift b/Sources/MdocDataTransfer18013/Protocols/MdocTransferManager.swift deleted file mode 100644 index 4a71549..0000000 --- a/Sources/MdocDataTransfer18013/Protocols/MdocTransferManager.swift +++ /dev/null @@ -1,198 +0,0 @@ -// -// MdocTransferManager.swift - -import Foundation -import ASN1Decoder -import SwiftCBOR -import MdocDataModel18013 -import MdocSecurity18013 -import Logging - -public typealias RequestItems = [String: [String: [String]]] -/// Protocol for a transfer manager object used to transfer data to and from the Mdoc holder. -public protocol MdocTransferManager: AnyObject { - func initialize(parameters: [String: Any]) - func performDeviceEngagement() - func stop() - var status: TransferStatus { get set } - var deviceEngagement: DeviceEngagement? { get } - var requireUserAccept: Bool { get set } - var sessionEncryption: SessionEncryption? { get set } - var deviceRequest: DeviceRequest? { get set } - var deviceResponseToSend: DeviceResponse? { get set } - var validRequestItems: RequestItems? { get set } - var errorRequestItems: RequestItems? { get set } - var delegate: MdocOfflineDelegate? { get set } - var docs: [DeviceResponse]! { get set } - var devicePrivateKey: CoseKeyPrivate! { get set } - var iaca: [SecCertificate]! { get set } - var error: Error? { get set } - var readerName: String? { get set } -} - -/// String keys for the initialization dictionary -public enum InitializeKeys: String { - case document_json_data - case document_signup_response_data - case device_private_key - case trusted_certificates - case require_user_accept -} - -/// String keys for the user request dictionary -public enum UserRequestKeys: String { - case valid_items_requested - case error_items_requested - case reader_certificate_issuer - case reader_auth_validated - case reader_certificate_validation_message -} - -extension MdocTransferManager { - - public func initialize(parameters: [String: Any]) { - if let d = parameters[InitializeKeys.document_json_data.rawValue] as? [Data] { - // load json sample data here - let sampleData = d.compactMap { $0.decodeJSON(type: SignUpResponse.self) } - docs = sampleData.compactMap { $0.deviceResponse } - devicePrivateKey = sampleData.compactMap { $0.devicePrivateKey }.first - } else if let drs = parameters[InitializeKeys.document_signup_response_data.rawValue] as? [DeviceResponse], let dpk = parameters[InitializeKeys.device_private_key.rawValue] as? CoseKeyPrivate { - docs = drs - devicePrivateKey = dpk - } - if docs == nil { error = Self.makeError(code: .documents_not_provided); return } - if docs.count == 0 { error = Self.makeError(code: .invalidInputDocument); return } - if devicePrivateKey == nil { error = Self.makeError(code: .device_private_key_not_provided); return } - if let i = parameters[InitializeKeys.trusted_certificates.rawValue] as? [Data] { - iaca = i.compactMap { SecCertificateCreateWithData(nil, $0 as CFData) } - } - if let b = parameters[InitializeKeys.require_user_accept.rawValue] as? Bool { - requireUserAccept = b - } - status = .initialized - } - - /// Decrypt the contents of a data object and return a ``DeviceRequest`` object if the data represents a valid device request. If the data does not represent a valid device request, the function returns nil. - /// - Parameters: - /// - requestData: Request data passed to the mdoc holder - /// - handler: Handler to call with the accept/reject flag - /// - devicePrivateKey: Device private key - /// - readerKeyRawData: reader key cbor data (if reader engagement is used) - /// - Returns: A ``DeviceRequest`` object - public func decodeRequestAndInformUser(requestData: Data, devicePrivateKey: CoseKeyPrivate, readerKeyRawData: [UInt8]?, handOver: CBOR, handler: @escaping (Bool, RequestItems?) -> Void) -> DeviceRequest? { - do { - guard let seCbor = try CBOR.decode([UInt8](requestData)) else { logger.error("Request Data is not Cbor"); return nil } - guard var se = SessionEstablishment(cbor: seCbor) else { logger.error("Request Data cannot be decoded to session establisment"); return nil } - if se.eReaderKeyRawData == nil, let readerKeyRawData { se.eReaderKeyRawData = readerKeyRawData } - guard se.eReaderKey != nil else { logger.error("Reader key not available"); return nil } - let requestCipherData = se.data - guard let deviceEngagement else { logger.error("Device Engagement not initialized"); return nil } - // init session-encryption object from session establish message and device engagement, decrypt data - sessionEncryption = SessionEncryption(se: se, de: deviceEngagement, handOver: handOver) - guard var sessionEncryption else { logger.error("Session Encryption not initialized"); return nil } - guard let requestData = try sessionEncryption.decrypt(requestCipherData) else { logger.error("Request data cannot be decrypted"); return nil } - guard let deviceRequest = DeviceRequest(data: requestData) else { logger.error("Decrypted data cannot be decoded"); return nil } - try getDeviceResponseToSend(deviceRequest, selectedItems: nil, eReaderKey: sessionEncryption.sessionKeys.publicKey, devicePrivateKey: devicePrivateKey) - guard let validRequestItems, let errorRequestItems else { logger.error("Valid request items nil"); return nil } - var params: [String: Any] = [UserRequestKeys.valid_items_requested.rawValue: validRequestItems, UserRequestKeys.error_items_requested.rawValue: errorRequestItems] - if let docR = deviceRequest.docRequests.first { - let mdocAuth = MdocReaderAuthentication(transcript: sessionEncryption.transcript) - if let readerAuthRawCBOR = docR.readerAuthRawCBOR, let certData = docR.readerCertificate, let x509 = try? X509Certificate(der: certData), let issName = x509.issuerDistinguishedName, let (b,reasonFailure) = try? mdocAuth.validateReaderAuth(readerAuthCBOR: readerAuthRawCBOR, readerAuthCertificate: certData, itemsRequestRawData: docR.itemsRequestRawData!, rootCerts: iaca) { - params[UserRequestKeys.reader_certificate_issuer.rawValue] = issName - params[UserRequestKeys.reader_auth_validated.rawValue] = b - if let reasonFailure { params[UserRequestKeys.reader_certificate_validation_message.rawValue] = reasonFailure } - } - } - self.deviceRequest = deviceRequest - if requireUserAccept { delegate?.didReceiveRequest(params, handleSelected: handler) } - return deviceRequest - } catch { self.error = error} - return nil - } - - @discardableResult public func getDeviceResponseToSend(_ deviceRequest: DeviceRequest?, selectedItems: RequestItems?, eReaderKey: CoseKey?, devicePrivateKey: CoseKeyPrivate) throws -> DeviceResponse? { - let documents = docs.flatMap { $0.documents! } - var docFiltered = [Document](); var docErrors = [[DocType: UInt64]]() - var validReqItemsDocDict = RequestItems(); var errorReqItemsDocDict = RequestItems() - guard deviceRequest != nil || selectedItems != nil else { fatalError("Invalid call") } - let haveDeviceRequest = deviceRequest != nil - let reqDocTypes = haveDeviceRequest ? deviceRequest!.docRequests.map(\.itemsRequest.docType) : Array(selectedItems!.keys) - for reqDocType in reqDocTypes { - let docReq = deviceRequest?.docRequests.findDoc(name: reqDocType) - guard let doc = documents.findDoc(name: reqDocType) else { - docErrors.append([reqDocType: UInt64(0)]) - errorReqItemsDocDict[reqDocType] = [:] - continue - } - guard let issuerNs = doc.issuerSigned.issuerNameSpaces else { logger.error("Null issuer namespaces"); return nil } - var nsItemsToAdd = [NameSpace: [IssuerSignedItem]]() - var nsErrorsToAdd = [NameSpace : ErrorItems]() - var validReqItemsNsDict = [NameSpace: [String]]() - // for each request namespace - let reqNamespaces = haveDeviceRequest ? Array(docReq!.itemsRequest.requestNameSpaces.nameSpaces.keys) : Array(selectedItems![reqDocType]!.keys) - for reqNamespace in reqNamespaces { - let reqElementIdentifiers = haveDeviceRequest ? docReq!.itemsRequest.requestNameSpaces.nameSpaces[reqNamespace]!.elementIdentifiers : Array(selectedItems![reqDocType]![reqNamespace]!) - guard let items = issuerNs[reqNamespace] else { - nsErrorsToAdd[reqNamespace] = Dictionary(grouping: reqElementIdentifiers, by: {$0}).mapValues { _ in 0 } - continue - } - let itemsReqSet = Set(reqElementIdentifiers).subtracting(IsoMdlModel.self.moreThan2AgeOverElementIdentifiers(reqDocType, reqNamespace, SimpleAgeAttest(namespaces: issuerNs.nameSpaces), reqElementIdentifiers)) - let itemsSet = Set(items.map(\.elementIdentifier)) - var itemsToAdd = items.filter({ itemsReqSet.contains($0.elementIdentifier) }) - if let selectedItems { - let selectedNsItems = selectedItems[reqDocType]?[reqNamespace] ?? [] - itemsToAdd = itemsToAdd.filter({ selectedNsItems.contains($0.elementIdentifier) }) - } - if itemsToAdd.count > 0 { - nsItemsToAdd[reqNamespace] = itemsToAdd - validReqItemsNsDict[reqNamespace] = itemsToAdd.map(\.elementIdentifier) - } - let errorItemsSet = itemsReqSet.subtracting(itemsSet) - if errorItemsSet.count > 0 { - nsErrorsToAdd[reqNamespace] = Dictionary(grouping: errorItemsSet, by: { $0 }).mapValues { _ in 0 } - } - } // end ns for - let issuerAuthToAdd = doc.issuerSigned.issuerAuth - let issToAdd = IssuerSigned(issuerNameSpaces: IssuerNameSpaces(nameSpaces: nsItemsToAdd), issuerAuth: issuerAuthToAdd) - var devSignedToAdd: DeviceSigned? = nil - if let eReaderKey, let sessionEncryption { - let authKeys = CoseKeyExchange(publicKey: eReaderKey, privateKey: devicePrivateKey) - let mdocAuth = MdocAuthentication(transcript: sessionEncryption.transcript, authKeys: authKeys) - guard let devAuth = try mdocAuth.getDeviceAuthForTransfer(docType: reqDocType) else {logger.error("Cannot create device auth"); return nil } - devSignedToAdd = DeviceSigned(deviceAuth: devAuth) - } - let errors: Errors? = nsErrorsToAdd.count == 0 ? nil : Errors(errors: nsErrorsToAdd) - let docToAdd = Document(docType: reqDocType, issuerSigned: issToAdd, deviceSigned: devSignedToAdd, errors: errors) - docFiltered.append(docToAdd) - validReqItemsDocDict[reqDocType] = validReqItemsNsDict - errorReqItemsDocDict[reqDocType] = nsErrorsToAdd.mapValues { Array($0.keys) } - } // end doc for - let documentErrors: [DocumentError]? = docErrors.count == 0 ? nil : docErrors.map(DocumentError.init(docErrors:)) - deviceResponseToSend = DeviceResponse(version: docs.first!.version, documents: docFiltered, documentErrors: documentErrors, status: 0) - validRequestItems = validReqItemsDocDict; errorRequestItems = errorReqItemsDocDict - return deviceResponseToSend - } - - public func getSessionDataToSend(_ deviceRequest: DeviceRequest, eReaderKey: CoseKey) -> Data? { - do { - guard var sessionEncryption else { logger.error("Session Encryption not initialized"); return nil } - guard let docToSend = deviceResponseToSend else { logger.error("Response to send not created"); return nil } - if docToSend.documents == nil { logger.error("Could not create documents to send") } - let cborToSend = docToSend.toCBOR(options: CBOROptions()) - let clearBytesToSend = cborToSend.encode() - guard let cipherData = try sessionEncryption.encrypt(clearBytesToSend) else { return nil } - let sd = SessionData(cipher_data: status == .error ? nil : cipherData, status: status == .error ? 0 : 20) - return Data(sd.encode(options: CBOROptions())) - } catch { self.error = error} - return nil - } - - - - public static func makeError(code: ErrorCode, str: String? = nil) -> NSError { - let errorMessage = str ?? NSLocalizedString(code.description, comment: code.description) - logger.error(Logger.Message(unicodeScalarLiteral: errorMessage)) - return NSError(domain: "\(MdocGattServer.self)", code: code.rawValue, userInfo: [NSLocalizedDescriptionKey: errorMessage]) - } - -} diff --git a/Sources/MdocDataTransfer18013/ViewModels/DefaultTransferDelegate.swift b/Sources/MdocDataTransfer18013/ViewModels/DefaultTransferDelegate.swift deleted file mode 100644 index ef59fe9..0000000 --- a/Sources/MdocDataTransfer18013/ViewModels/DefaultTransferDelegate.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TransferDelegateObject.swift -// Iso18013HolderDemo - -import Foundation -import SwiftUI - -public class DefaultTransferDelegate: ObservableObject, MdocOfflineDelegate { - public init(readerCertIsserMessage: String? = nil, readerCertValidationMessage: String? = nil, hasError: Bool = false, errorMessage: String = "", selectedRequestItems: [DocElementsViewModel] = [], handleSelected: @escaping (Bool, RequestItems?) -> Void = { _,_ in }) { - self.readerCertIsserMessage = readerCertIsserMessage - self.readerCertValidationMessage = readerCertValidationMessage - self.hasError = hasError - self.errorMessage = errorMessage - self.selectedRequestItems = selectedRequestItems - self.handleSelected = handleSelected - } - - - @Published public var readerCertIsserMessage: String? - @Published public var readerCertValidationMessage: String? - @Published public var hasError: Bool = false - @Published public var errorMessage: String = "" - @Published public var selectedRequestItems: [DocElementsViewModel] = [] - @Published public var status: TransferStatus = .initializing - public var handleSelected: (Bool, RequestItems?) -> Void = { _,_ in } - - public func didChangeStatus(_ newStatus: TransferStatus) { - status = newStatus - } - - public func didReceiveRequest(_ request: [String: Any], handleSelected: @escaping (Bool, RequestItems?) -> Void) { - self.handleSelected = handleSelected - // show the items as checkboxes - guard let validRequestItems = request[UserRequestKeys.valid_items_requested.rawValue] as? RequestItems else { return } - var tmp = validRequestItems.toDocElementViewModels(valid: true) - if let errorRequestItems = request[UserRequestKeys.error_items_requested.rawValue] as? RequestItems, errorRequestItems.count > 0 { - tmp = tmp.merging(with: errorRequestItems.toDocElementViewModels(valid: false)) - } - selectedRequestItems = tmp - if let readerAuthority = request[UserRequestKeys.reader_certificate_issuer.rawValue] as? String { - let bAuthenticated = request[UserRequestKeys.reader_auth_validated.rawValue] as? Bool ?? false - readerCertIsserMessage = "Reader Certificate Issuer:\n\(readerAuthority)\n\(bAuthenticated ? "Authenticated" : "NOT authenticated")\n\(request[UserRequestKeys.reader_certificate_validation_message.rawValue] as? String ?? "")" - } - } - - public func didFinishedWithError(_ error: Error) { - hasError = true - errorMessage = error.localizedDescription - } - - -} diff --git a/Sources/MdocDataTransfer18013/ViewModels/DocElementsViewModel.swift b/Sources/MdocDataTransfer18013/ViewModels/DocElementsViewModel.swift deleted file mode 100644 index 4d808f6..0000000 --- a/Sources/MdocDataTransfer18013/ViewModels/DocElementsViewModel.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// ElementViewModel.swift -// Iso18013HolderDemo -// -// Created by ffeli on 04/09/2023. -// Copyright © 2023 EUDIW. All rights reserved. -// - -import Foundation - -public struct DocElementsViewModel: Identifiable { - public var id: String { docType } - public let docType: String - public var isEnabled: Bool - public var elements: [ElementViewModel] -} - -func fluttenItemViewModels(_ nsItems: [String:[String]], valid isEnabled: Bool) -> [ElementViewModel] { - nsItems.map { k,v in nsItemsToViewModels(k,v, isEnabled) }.flatMap {$0} -} - -func nsItemsToViewModels(_ ns: String, _ items: [String], _ isEnabled: Bool) -> [ElementViewModel] { - items.map { ElementViewModel(nameSpace: ns, elementIdentifier:$0, isEnabled: isEnabled) } -} - -extension RequestItems { - func toDocElementViewModels(valid: Bool) -> [DocElementsViewModel] { - map { docType,nsItems in DocElementsViewModel(docType: docType, isEnabled: valid, elements: fluttenItemViewModels(nsItems, valid: valid)) } - } -} - -extension Array where Element == DocElementsViewModel { - public var docSelectedDictionary: RequestItems { Dictionary(grouping: self, by: \.docType).mapValues { $0.first!.elements.filter(\.isSelected).nsDictionary } } - - func merging(with other: Self) -> Self { - var res = Self() - for otherDE in other { - if let exist = first(where: { $0.docType == otherDE.docType}) { - let newElements = (exist.elements + otherDE.elements).sorted(by: { $0.isEnabled && $1.isDisabled }) - res.append(DocElementsViewModel(docType: exist.docType, isEnabled: exist.isEnabled, elements: newElements)) - } - else { res.append(otherDE) } - } - return res - } -} - -public struct ElementViewModel: Identifiable { - public var id: String { "\(nameSpace)_\(elementIdentifier)" } - public let nameSpace: String - public let elementIdentifier: String - public var isEnabled: Bool - public var isDisabled: Bool { !isEnabled } - public var isSelected = true -} - -extension Array where Element == ElementViewModel { - var nsDictionary: [String: [String]] { Dictionary(grouping: self, by: \.nameSpace).mapValues { $0.map(\.elementIdentifier)} } -}