diff --git a/Info.plist b/Info.plist index bf9482a..af5189f 100644 --- a/Info.plist +++ b/Info.plist @@ -16,8 +16,6 @@ 1.0 CFBundleVersion 1 - NSCameraUsageDescription - QR Code Scanner NSBluetoothAlwaysUsageDescription Secure transmission of mobile DL data diff --git a/Package.swift b/Package.swift index 9309b37..b63e3ad 100644 --- a/Package.swift +++ b/Package.swift @@ -14,8 +14,8 @@ let package = Package( targets: ["SpruceIDWalletSdk"]) ], dependencies: [ - .package(url: "https://github.com/spruceid/wallet-sdk-rs.git", from: "0.0.25"), - // .package(path: "../wallet-sdk-rs"), + // .package(url: "https://github.com/spruceid/wallet-sdk-rs.git", from: "0.0.25"), + .package(path: "../wallet-sdk-rs"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], targets: [ diff --git a/Sources/WalletSdk/MDocBLEUtils.swift b/Sources/WalletSdk/MDocBLEUtils.swift index a1ea982..814de2d 100644 --- a/Sources/WalletSdk/MDocBLEUtils.swift +++ b/Sources/WalletSdk/MDocBLEUtils.swift @@ -1,4 +1,5 @@ import CoreBluetooth +import SpruceIDWalletSdkRs let holderStateCharacteristicId = CBUUID(string: "00000001-A123-48CE-896B-4C76973373E6") let holderClient2ServerCharacteristicId = CBUUID(string: "00000002-A123-48CE-896B-4C76973373E6") @@ -18,6 +19,13 @@ enum MdocHolderBleError { case bluetooth(CBCentralManager) } +enum MdocReaderBleError { + /// When communication with the server fails + case server(String) + /// When Bluetooth is unusable (e.g. unauthorized). + case bluetooth(CBCentralManager) +} + enum MDocBLECallback { case done case connected @@ -30,3 +38,16 @@ enum MDocBLECallback { protocol MDocBLEDelegate: AnyObject { func callback(message: MDocBLECallback) } + +enum MDocReaderBLECallback { + case done([String: [String: [String: MDocItem]]]) + case connected + case error(MdocReaderBleError) + case message(Data) + /// Chunks received so far + case downloadProgress(Int) +} + +protocol MDocReaderBLEDelegate: AnyObject { + func callback(message: MDocReaderBLECallback) +} diff --git a/Sources/WalletSdk/MDocHolderBLECentral.swift b/Sources/WalletSdk/MDocHolderBLECentral.swift index 9f83d3a..9ff8897 100644 --- a/Sources/WalletSdk/MDocHolderBLECentral.swift +++ b/Sources/WalletSdk/MDocHolderBLECentral.swift @@ -189,6 +189,7 @@ class MDocHolderBLECentral: NSObject { case let .some(byte): throw DataError.unknownDataTransferPrefix(byte: byte) } + // Looks like this should just happen after discovering characteristics case readerIdentCharacteristicId: self.peripheral?.setNotifyValue(true, for: self.readCharacteristic!) self.peripheral?.setNotifyValue(true, for: self.stateCharacteristic!) diff --git a/Sources/WalletSdk/MDocReader.swift b/Sources/WalletSdk/MDocReader.swift new file mode 100644 index 0000000..14ba8ac --- /dev/null +++ b/Sources/WalletSdk/MDocReader.swift @@ -0,0 +1,87 @@ +import CoreBluetooth +import SpruceIDWalletSdkRs + +public class MDocReader { + var sessionManager: MdlSessionManager + var bleManager: MDocReaderBLEPeripheral! + var callback: BLEReaderSessionStateDelegate + + public init?(callback: BLEReaderSessionStateDelegate, uri: String, requestedItems: [String: [String: Bool]]) { + self.callback = callback + do { + let sessionData = try SpruceIDWalletSdkRs.establishSession(uri: uri, requestedItems: requestedItems, trustAnchorRegistry: nil) + self.sessionManager = sessionData.state + self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, bleIdent: Data(sessionData.bleIdent.utf8)) + } catch { + print("\(error)") + return nil + } + } + + public func cancel() { + bleManager.disconnect() + } +} + +extension MDocReader: MDocReaderBLEDelegate { + func callback(message: MDocReaderBLECallback) { + switch message { + case .done(let data): + self.callback.update(state: .success(data)) + case .connected: + self.callback.update(state: .connected) + case .error(let error): + self.callback.update(state: .error(BleReaderSessionError(readerBleError: error))) + self.cancel() + case .message(let data): + do { + let responseData = try SpruceIDWalletSdkRs.handleResponse(state: self.sessionManager, response: data) + self.sessionManager = responseData.state + self.callback.update(state: .success(responseData.verifiedResponse)) + } catch { + self.callback.update(state: .error(.generic("\(error)"))) + self.cancel() + } + case .downloadProgress(let index): + self.callback.update(state: .downloadProgress(index)) + } + } +} + +/// To be implemented by the consumer to update the UI +public protocol BLEReaderSessionStateDelegate: AnyObject { + func update(state: BLEReaderSessionState) +} + +public enum BLEReaderSessionState { + /// App should display the error message + case error(BleReaderSessionError) + /// App should indicate to the reader is waiting to connect to the holder + case advertizing + /// App should indicate to the user that BLE connection has been established + case connected + /// App should display the fact that a certain amount of data has been received + /// - Parameters: + /// - 0: The number of chunks received to far + case downloadProgress(Int) + /// App should display a success message and offer to close the page + case success([String: [String: [String: MDocItem]]]) +} + +public enum BleReaderSessionError { + /// When communication with the server fails + case server(String) + /// When Bluetooth is unusable (e.g. unauthorized). + case bluetooth(CBCentralManager) + /// Generic unrecoverable error + case generic(String) + + init(readerBleError: MdocReaderBleError) { + switch readerBleError { + case .server(let string): + self = .server(string) + case .bluetooth(let string): + self = .bluetooth(string) + } + } +} diff --git a/Sources/WalletSdk/MDocReaderBLEPeripheral.swift b/Sources/WalletSdk/MDocReaderBLEPeripheral.swift new file mode 100644 index 0000000..d3d512a --- /dev/null +++ b/Sources/WalletSdk/MDocReaderBLEPeripheral.swift @@ -0,0 +1,232 @@ +import Algorithms +import CoreBluetooth +import Foundation +import SpruceIDWalletSdkRs + +class MDocReaderBLEPeripheral: NSObject { + var peripheralManager: CBPeripheralManager! + var serviceUuid: CBUUID + var bleIdent: Data + var incomingMessageBuffer = Data() + var incomingMessageIndex = 0 + var callback: MDocReaderBLEDelegate + var writeCharacteristic: CBMutableCharacteristic? + var readCharacteristic: CBMutableCharacteristic? + var stateCharacteristic: CBMutableCharacteristic? + var identCharacteristic: CBMutableCharacteristic? + var l2capCharacteristic: CBMutableCharacteristic? + var requestData: Data + var maximumCharacteristicSize: Int? + var writingQueueTotalChunks: Int? + var writingQueueChunkIndex: Int? + var writingQueue: IndexingIterator>? + + init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data) { + self.serviceUuid = serviceUuid + self.callback = callback + self.bleIdent = bleIdent + self.requestData = request + self.incomingMessageBuffer = Data() + super.init() + self.peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) + } + + func setupService() { + let service = CBMutableService(type: self.serviceUuid, primary: true) + // CBUUIDClientCharacteristicConfigurationString only returns "2902" + // let clientDescriptor = CBMutableDescriptor(type: CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb"), value: Data([0x00, 0x00])) as CBDescriptor + // wallet-sdk-kt isn't using write without response... + self.stateCharacteristic = CBMutableCharacteristic(type: readerStateCharacteristicId, + properties: [.notify, .writeWithoutResponse, .write], + value: nil, + permissions: [.writeable]) + // for some reason this seems to drop all other descriptors + // self.stateCharacteristic!.descriptors = [clientDescriptor] + (self.stateCharacteristic!.descriptors ?? [] ) + // self.stateCharacteristic!.descriptors?.insert(clientDescriptor, at: 0) + // wallet-sdk-kt isn't using write without response... + self.readCharacteristic = CBMutableCharacteristic(type: readerClient2ServerCharacteristicId, + properties: [.writeWithoutResponse, .write], + value: nil, + permissions: [.writeable]) + self.writeCharacteristic = CBMutableCharacteristic(type: readerServer2ClientCharacteristicId, + properties: [.notify], + value: nil, + permissions: [.readable, .writeable]) + // self.writeCharacteristic!.descriptors = [clientDescriptor] + (self.writeCharacteristic!.descriptors ?? [] ) + // self.writeCharacteristic!.descriptors?.insert(clientDescriptor, at: 0) + self.identCharacteristic = CBMutableCharacteristic(type: readerIdentCharacteristicId, + properties: [.read], + value: bleIdent, + permissions: [.readable]) + // wallet-sdk-kt is failing if this is present + // self.l2capCharacteristic = CBMutableCharacteristic(type: readerL2CAPCharacteristicId, + // properties: [.read], + // value: nil, + // permissions: [.readable]) + service.characteristics = (service.characteristics ?? []) + [ + stateCharacteristic! as CBCharacteristic, + readCharacteristic! as CBCharacteristic, + writeCharacteristic! as CBCharacteristic, + identCharacteristic! as CBCharacteristic, + // l2capCharacteristic! as CBCharacteristic + ] + peripheralManager.add(service) + } + + func disconnect() { + return + } + + func writeOutgoingValue(data: Data) { + let chunks = data.chunks(ofCount: maximumCharacteristicSize! - 1) + writingQueueTotalChunks = chunks.count + writingQueue = chunks.makeIterator() + writingQueueChunkIndex = 0 + drainWritingQueue() + } + + private func drainWritingQueue() { + if writingQueue != nil { + if var chunk = writingQueue?.next() { + var firstByte: Data.Element + writingQueueChunkIndex! += 1 + if writingQueueChunkIndex == writingQueueTotalChunks { + firstByte = 0x00 + } else { + firstByte = 0x01 + } + chunk.reverse() + chunk.append(firstByte) + chunk.reverse() + self.peripheralManager?.updateValue(chunk, for: self.writeCharacteristic!, onSubscribedCentrals: nil) + } else { + writingQueue = nil + } + } + } + + func processData(central: CBCentral, characteristic: CBCharacteristic, value: Data?) throws { + if var data = value { + print("Processing data for \(characteristic.uuid)") + switch characteristic.uuid { + case readerClient2ServerCharacteristicId: + let firstByte = data.popFirst() + incomingMessageBuffer.append(data) + switch firstByte { + case .none: + throw DataError.noData(characteristic: characteristic.uuid) + case 0x00: // end + print("End of message") + self.callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) + self.incomingMessageBuffer = Data() + self.incomingMessageIndex = 0 + return + case 0x01: // partial + print("Partial message") + self.incomingMessageIndex += 1 + self.callback.callback(message: .downloadProgress(self.incomingMessageIndex)) + // TODO check length against MTU + return + case let .some(byte): + throw DataError.unknownDataTransferPrefix(byte: byte) + } + case readerStateCharacteristicId: + if data.count != 1 { + throw DataError.invalidStateLength + } + switch data[0] { + case 0x01: + print("Starting to send request") + writeOutgoingValue(data: self.requestData) + case let byte: + throw DataError.unknownState(byte: byte) + } + return +// case readerL2CAPCharacteristicId: +// return + case let uuid: + throw DataError.unknownCharacteristic(uuid: uuid) + } + } else { + throw DataError.noData(characteristic: characteristic.uuid) + } + } +} + +extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { + func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { + switch peripheral.state { + case .poweredOn: + print("Advertising...") + setupService() + peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [serviceUuid]]) + case .unsupported: + print("Peripheral Is Unsupported.") + case .unauthorized: + print("Peripheral Is Unauthorized.") + case .unknown: + print("Peripheral Unknown") + case .resetting: + print("Peripheral Resetting") + case .poweredOff: + print("Peripheral Is Powered Off.") + @unknown default: + print("Error") + } + } + + // This is called when there is space in the queue again (so it is part of the loop for drainWritingQueue) + func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { + self.drainWritingQueue() + } + + func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { + print("Subscribed to \(characteristic.uuid)") + self.callback.callback(message: .connected) + self.peripheralManager?.stopAdvertising() + switch characteristic.uuid { + case readerStateCharacteristicId: + // This will trigger wallet-sdk-swift to send 0x01 to start the exchange + peripheralManager.updateValue(bleIdent, for: self.identCharacteristic!, onSubscribedCentrals: nil) + // This will trigger wallet-sdk-kt to send 0x01 to start the exchange + peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + case _: + return + } + } + + func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { + print("Received read request for \(request.characteristic.uuid)") + + // Since there is no callback for MTU on iOS we will grab it here. + maximumCharacteristicSize = min(request.central.maximumUpdateValueLength, 512) + + if (request.characteristic.uuid == readerIdentCharacteristicId) { + peripheralManager.respond(to: request, withResult: .success) + } else if (request.characteristic.uuid == readerL2CAPCharacteristicId) { +// peripheralManager.publishL2CAPChannel(withEncryption: true) +// peripheralManager.respond(to: request, withResult: .success) + } else { + self.callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(request.characteristic.uuid)"))) + } + } + + func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { + for request in requests { + // Since there is no callback for MTU on iOS we will grab it here. + maximumCharacteristicSize = min(request.central.maximumUpdateValueLength, 512) + + do { + print("Processing request") + try processData(central: request.central, characteristic: request.characteristic, value: request.value) + // This can be removed, or return an error, once wallet-sdk-kt is fixed and uses withoutResponse writes + if request.characteristic.properties.contains(.write) { + peripheralManager.respond(to: request, withResult: .success) + } + } catch { + self.callback.callback(message: .error(.server("\(error)"))) + self.peripheralManager?.updateValue(Data([0x02]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + } + } + } +} diff --git a/project.yml b/project.yml index a1be228..c1eab87 100644 --- a/project.yml +++ b/project.yml @@ -4,8 +4,8 @@ options: packages: SpruceIDWalletSdkRs: url: https://github.com/spruceid/wallet-sdk-rs - from: 0.0.4 - # path: "../wallet-sdk-rs" + # from: 0.0.4 + path: "../wallet-sdk-rs" SwiftAlgorithms: url: https://github.com/apple/swift-algorithms from: 1.2.0