From b94d87f6bb0a95f44a0f66457a63d8e9d6551cb2 Mon Sep 17 00:00:00 2001 From: Simon Bihel Date: Mon, 29 Jul 2024 10:58:18 +0100 Subject: [PATCH 01/17] Add MDoc Reader support --- Info.plist | 22 ++ Package.swift | 5 +- Sources/MobileSdk/MDocBLEUtils.swift | 21 ++ Sources/MobileSdk/MDocHolderBLECentral.swift | 1 + Sources/MobileSdk/MDocReader.swift | 87 +++++++ .../MobileSdk/MDocReaderBLEPeripheral.swift | 232 ++++++++++++++++++ 6 files changed, 365 insertions(+), 3 deletions(-) create mode 100644 Info.plist create mode 100644 Sources/MobileSdk/MDocReader.swift create mode 100644 Sources/MobileSdk/MDocReaderBLEPeripheral.swift diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..af5189f --- /dev/null +++ b/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSBluetoothAlwaysUsageDescription + Secure transmission of mobile DL data + + diff --git a/Package.swift b/Package.swift index bbe3e98..b199afc 100644 --- a/Package.swift +++ b/Package.swift @@ -14,9 +14,8 @@ let package = Package( targets: ["SpruceIDMobileSdk"]) ], dependencies: [ - // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", .branch("main")), - .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.27"), - // .package(path: "../mobile-sdk-rs"), + // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", .branch("main")), + .package(path: "../mobile-sdk-rs"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], targets: [ diff --git a/Sources/MobileSdk/MDocBLEUtils.swift b/Sources/MobileSdk/MDocBLEUtils.swift index a1ea982..e609313 100644 --- a/Sources/MobileSdk/MDocBLEUtils.swift +++ b/Sources/MobileSdk/MDocBLEUtils.swift @@ -1,4 +1,5 @@ import CoreBluetooth +import SpruceIDMobileSdkRs 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/MobileSdk/MDocHolderBLECentral.swift b/Sources/MobileSdk/MDocHolderBLECentral.swift index be1086c..1c3b05c 100644 --- a/Sources/MobileSdk/MDocHolderBLECentral.swift +++ b/Sources/MobileSdk/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/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift new file mode 100644 index 0000000..14ba8ac --- /dev/null +++ b/Sources/MobileSdk/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/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift new file mode 100644 index 0000000..d3d512a --- /dev/null +++ b/Sources/MobileSdk/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) + } + } + } +} From 7b97fbc30d459f1cd5c3cd8947ef8a3aab461ac4 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 12:02:40 -0700 Subject: [PATCH 02/17] fix: remove WalletSdkRs dead referenced Signed-off-by: Ryan Tate --- Info.plist | 2 ++ Sources/MobileSdk/MDocReader.swift | 4 ++-- Sources/MobileSdk/MDocReaderBLEPeripheral.swift | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Info.plist b/Info.plist index af5189f..d04f29e 100644 --- a/Info.plist +++ b/Info.plist @@ -18,5 +18,7 @@ 1 NSBluetoothAlwaysUsageDescription Secure transmission of mobile DL data + NSCameraUsageDescription + QR Code Scanner diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index 14ba8ac..59fcc0a 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -1,5 +1,5 @@ import CoreBluetooth -import SpruceIDWalletSdkRs +import SpruceIDMobileSdkRs public class MDocReader { var sessionManager: MdlSessionManager @@ -9,7 +9,7 @@ public class MDocReader { 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) + let sessionData = try SpruceIDMobileSdkRs.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 { diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index d3d512a..7e55e46 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -1,7 +1,7 @@ import Algorithms import CoreBluetooth import Foundation -import SpruceIDWalletSdkRs +import SpruceIDMobileSdkRs class MDocReaderBLEPeripheral: NSObject { var peripheralManager: CBPeripheralManager! From 08506a626441ff4c5c74a32e868358474dcc91de Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 12:44:55 -0700 Subject: [PATCH 03/17] use mobile-sdk-rs local path for development Signed-off-by: Ryan Tate --- Sources/MobileSdk/MDocReader.swift | 2 +- project.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index 59fcc0a..6dc6e24 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -35,7 +35,7 @@ extension MDocReader: MDocReaderBLEDelegate { self.cancel() case .message(let data): do { - let responseData = try SpruceIDWalletSdkRs.handleResponse(state: self.sessionManager, response: data) + let responseData = try SpruceIDMobileSdkRs.handleResponse(state: self.sessionManager, response: data) self.sessionManager = responseData.state self.callback.update(state: .success(responseData.verifiedResponse)) } catch { diff --git a/project.yml b/project.yml index c856b5f..d3ec37c 100644 --- a/project.yml +++ b/project.yml @@ -3,9 +3,9 @@ options: bundleIdPrefix: com.spruceid.mobile.sdk packages: SpruceIDMobileSdkRs: - url: https://github.com/spruceid/mobile-sdk-rs - from: 0.0.27 - # path: "../mobile-sdk-rs" + # url: https://github.com/spruceid/mobile-sdk-rs + # from: 0.0.27 + path: "../mobile-sdk-rs" SwiftAlgorithms: url: https://github.com/apple/swift-algorithms from: 1.2.0 From 24a3c8d05344a77008d86286760ea1d34b1f0bba Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 12:50:32 -0700 Subject: [PATCH 04/17] use revision for mobile-sdk-rs for ci build Signed-off-by: Ryan Tate --- project.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/project.yml b/project.yml index d3ec37c..ffe17ba 100644 --- a/project.yml +++ b/project.yml @@ -3,9 +3,10 @@ options: bundleIdPrefix: com.spruceid.mobile.sdk packages: SpruceIDMobileSdkRs: - # url: https://github.com/spruceid/mobile-sdk-rs + url: https://github.com/spruceid/mobile-sdk-rs + revision: "820008ddd0e20c1c286d49e83967ef9362745902" # from: 0.0.27 - path: "../mobile-sdk-rs" + # path: "../mobile-sdk-rs" SwiftAlgorithms: url: https://github.com/apple/swift-algorithms from: 1.2.0 From 9ca858eaabba7318cc248c159185ac2fd2f19d9a Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 16:35:32 -0700 Subject: [PATCH 05/17] bump the target ios version to 14 Signed-off-by: Ryan Tate --- SpruceIDMobileSdk.podspec | 2 +- project.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SpruceIDMobileSdk.podspec b/SpruceIDMobileSdk.podspec index 5d177cc..a20e63c 100644 --- a/SpruceIDMobileSdk.podspec +++ b/SpruceIDMobileSdk.podspec @@ -14,7 +14,7 @@ Pod::Spec.new do |spec| spec.platform = :ios spec.swift_version = '5.9' - spec.ios.deployment_target = '13.0' + spec.ios.deployment_target = '14.0' spec.source = { :git => "https://github.com/spruceid/mobile-sdk-swift.git", :tag => "#{spec.version}" } spec.source_files = "Sources/MobileSdk/*.swift" diff --git a/project.yml b/project.yml index ffe17ba..f9d3c5c 100644 --- a/project.yml +++ b/project.yml @@ -21,7 +21,7 @@ targets: MobileSdk: type: library.dynamic platform: iOS - deploymentTarget: "13.0" + deploymentTarget: "14.0" sources: - Sources dependencies: From bb6a966fd39f42cca125b25467a36ef31957f096 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 16:38:23 -0700 Subject: [PATCH 06/17] use reversion tag for package dependency Signed-off-by: Ryan Tate --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 6eef9f0..a73d763 100644 --- a/Package.swift +++ b/Package.swift @@ -14,9 +14,9 @@ let package = Package( targets: ["SpruceIDMobileSdk"]) ], dependencies: [ - // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", .branch("main")), + .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", revision: "820008ddd0e20c1c286d49e83967ef9362745902"), // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.28"), - .package(path: "../mobile-sdk-rs"), + //.package(path: "../mobile-sdk-rs"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], targets: [ From 095bccf3902a5883db1a16c40b09b58575e318d9 Mon Sep 17 00:00:00 2001 From: Simon Bihel Date: Fri, 30 Aug 2024 13:25:48 +0100 Subject: [PATCH 07/17] Remove info.plist Not sure how it was checked out as it is in .gitignore --- Info.plist | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 Info.plist diff --git a/Info.plist b/Info.plist deleted file mode 100644 index d04f29e..0000000 --- a/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - NSBluetoothAlwaysUsageDescription - Secure transmission of mobile DL data - NSCameraUsageDescription - QR Code Scanner - - From fb8607a24475726401af1a1eaa11f9dfa3307cd8 Mon Sep 17 00:00:00 2001 From: Simon Bihel Date: Fri, 30 Aug 2024 17:28:32 +0100 Subject: [PATCH 08/17] Add support for trust anchor registry --- Sources/MobileSdk/MDocBLEUtils.swift | 2 +- Sources/MobileSdk/MDocReader.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/MobileSdk/MDocBLEUtils.swift b/Sources/MobileSdk/MDocBLEUtils.swift index e609313..cafc8ce 100644 --- a/Sources/MobileSdk/MDocBLEUtils.swift +++ b/Sources/MobileSdk/MDocBLEUtils.swift @@ -40,7 +40,7 @@ protocol MDocBLEDelegate: AnyObject { } enum MDocReaderBLECallback { - case done([String: [String: [String: MDocItem]]]) + case done([String: [String: MDocItem]]) case connected case error(MdocReaderBleError) case message(Data) diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index 6dc6e24..f4d5492 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -6,12 +6,12 @@ public class MDocReader { var bleManager: MDocReaderBLEPeripheral! var callback: BLEReaderSessionStateDelegate - public init?(callback: BLEReaderSessionStateDelegate, uri: String, requestedItems: [String: [String: Bool]]) { + public init?(callback: BLEReaderSessionStateDelegate, uri: String, requestedItems: [String: [String: Bool]], trustAnchorRegistry: [String]?) { self.callback = callback do { - let sessionData = try SpruceIDMobileSdkRs.establishSession(uri: uri, requestedItems: requestedItems, trustAnchorRegistry: nil) + let sessionData = try SpruceIDMobileSdkRs.establishSession(uri: uri, requestedItems: requestedItems, trustAnchorRegistry: trustAnchorRegistry) self.sessionManager = sessionData.state - self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, bleIdent: Data(sessionData.bleIdent.utf8)) + self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, bleIdent: sessionData.bleIdent) } catch { print("\(error)") return nil @@ -65,7 +65,7 @@ public enum BLEReaderSessionState { /// - 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]]]) + case success([String: [String: MDocItem]]) } public enum BleReaderSessionError { From 8e65c002ca338802ea09bbf2e85070b4253ff5f1 Mon Sep 17 00:00:00 2001 From: Simon Bihel Date: Fri, 30 Aug 2024 17:43:03 +0100 Subject: [PATCH 09/17] Local paths for local testing --- Package.resolved | 9 --------- Package.swift | 3 +-- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Package.resolved b/Package.resolved index 0670210..0c23b4e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "mobile-sdk-rs", - "kind" : "remoteSourceControl", - "location" : "https://github.com/spruceid/mobile-sdk-rs.git", - "state" : { - "revision" : "9deb085da5d26630045505cad6f46f11460d739d", - "version" : "0.0.27" - } - }, { "identity" : "swift-algorithms", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index a73d763..ed9a737 100644 --- a/Package.swift +++ b/Package.swift @@ -14,9 +14,8 @@ let package = Package( targets: ["SpruceIDMobileSdk"]) ], dependencies: [ - .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", revision: "820008ddd0e20c1c286d49e83967ef9362745902"), // .package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.28"), - //.package(path: "../mobile-sdk-rs"), + .package(path: "../mobile-sdk-rs"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0") ], targets: [ From a7253f5c2d1adde5f837ff2effb867f590ab6186 Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Mon, 2 Sep 2024 15:53:50 -0400 Subject: [PATCH 10/17] iOS reader/holder now support L2CAP. This is a fairly large change; both the holder and the reader have been extended to support L2CAP. In service of this, the original flow has been refactored into more explicit state machines, and the L2CAP flow has been added to those state machines. Both the reader and holder have `useL2CAP` booleans; if either is `false`, the reader and holder will use the old flow to communicate. This should also mean that they will use the old flow when working with other readers or holders that do not support L2CAP. Some of this work (notably the `*Connection.swift` files) is derived from Paul Wilkinson's MIT-licensed L2Cap library: https://github.com/paulw11/L2Cap This repo is MIT-licensed as well, so the licenses are compatable. We aren't using the L2Cap library as-is for a variety of reasons, but the main reason is that the behaviour we need differs significantly from the behaviour L2Cap (the library) offers. There are a couple of places in this change that are hacking around impedence mismatches: - we seem to need to have `notify` on the L2CAP characteristic in order to propagate the PSM to central, though 18013-5 Annex A claims the only required property is `read` -- we're not out of spec, since we're offering an optional property, but it remains to be seen how that will interact with 3rd party centrals - 18013-5 specifies no framing for the request or response sent over the L2CAP stream, so we have to infer when the data has arrived from timing; this seems like a fragile method particularly if confronted by noisy radio environments --- Sources/MobileSdk/BLEConnection.swift | 178 +++++++++ Sources/MobileSdk/MDocBLEUtils.swift | 65 ++++ Sources/MobileSdk/MDocHolderBLECentral.swift | 356 ++++++++++++++---- .../MDocHolderBLECentralConnection.swift | 68 ++++ .../MobileSdk/MDocReaderBLEPeripheral.swift | 327 ++++++++++++++-- .../MDocReaderBLEPeripheralConnection.swift | 62 +++ 6 files changed, 949 insertions(+), 107 deletions(-) create mode 100644 Sources/MobileSdk/BLEConnection.swift create mode 100644 Sources/MobileSdk/MDocHolderBLECentralConnection.swift create mode 100644 Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift diff --git a/Sources/MobileSdk/BLEConnection.swift b/Sources/MobileSdk/BLEConnection.swift new file mode 100644 index 0000000..0d0e09b --- /dev/null +++ b/Sources/MobileSdk/BLEConnection.swift @@ -0,0 +1,178 @@ +// Derived from MIT-licensed work by Paul Wilkinson: https://github.com/paulw11/L2Cap + +import CoreBluetooth +import Foundation + +/// The base BLE connection, only intended for subclassing. +class BLEInternalConnection: NSObject, StreamDelegate { + var channel: CBL2CAPChannel? + + private var outputData = Data() + private var incomingData = Data() + private var incomingTime = Date(timeIntervalSinceNow: 0) + private var incomingDelivered = false + private var openCount = 0 + private var totalBytesWritten = 0 + + /// Handle stream events. Many of these we hand to local methods which the child classes are expected to + /// override. + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + switch eventCode { + case Stream.Event.openCompleted: + // TODO: This is a bit of a hack, but it'll do for now. There are two streams, one input, one + // output, and we get notified about both. We really only want to start doing things when + // both are available. + openCount += 1 + + if openCount == 2 { + streamIsOpen() + } + + case Stream.Event.endEncountered: + openCount -= 1 + streamEnded() + + case Stream.Event.hasBytesAvailable: + streamBytesAvailable() + readBytes(from: aStream as! InputStream) + + case Stream.Event.hasSpaceAvailable: + streamSpaceAvailable() + send() + + case Stream.Event.errorOccurred: + streamError() + + default: + streamUnknownEvent() + } + } + + /// Public send() interface. + public func send(data: Data) { + outputData.append(data) + totalBytesWritten = 0 + send() + } + + /// Internal send() interface. + private func send() { + guard let ostream = channel?.outputStream, !outputData.isEmpty, ostream.hasSpaceAvailable else { + return + } + let bytesWritten = ostream.write(outputData) + + totalBytesWritten += bytesWritten + + // The isEmpty guard above should prevent div0 errors here. + let fracDone = Double(totalBytesWritten) / Double(outputData.count) + + streamSentData(bytes: bytesWritten, total: totalBytesWritten, fraction: fracDone) + + if bytesWritten < outputData.count { + outputData = outputData.advanced(by: bytesWritten) + } else { + outputData.removeAll() + } + } + + /// Close the stream. + public func close() { + if let ch = channel { + ch.outputStream.close() + ch.inputStream.close() + ch.inputStream.remove(from: .main, forMode: .default) + ch.outputStream.remove(from: .main, forMode: .default) + ch.inputStream.delegate = nil + ch.outputStream.delegate = nil + openCount = 0 + } + + channel = nil + } + + /// Read from the stream. + private func readBytes(from stream: InputStream) { + let bufLength = 1024 + let buffer = UnsafeMutablePointer.allocate(capacity: bufLength) + defer { + buffer.deallocate() + } + let bytesRead = stream.read(buffer, maxLength: bufLength) + incomingData.append(buffer, count: bytesRead) + + // This is an awful hack to work around a hairy problem. L2CAP is a stream protocol; there's + // no framing on data, so there's no way to signal that the data exchange is complete. In principle + // we could build a framing protocol on top, or we could use the State characteristics to signal out + // of band, but neither of those are specified by the spec, so we'd be out of compliance. The State + // signalling is what the non-L2CAP flow uses, but the spec explicitly says it's not used with L2CAP. + // + // Another thing we could do would be close the connection, but there are two problems with that; + // the first is we'd be out of spec compliance again, and the second is that we actually have two + // messages going, one in each direction, serially. If we closed to indicate the length of the first, + // we'd have no connection for the second. + // + // So, we have data coming in, and we don't know how much. The stream lets us know when more data + // has arrived, the data comes in chunks. What we do, then, is timestamp when we receive some data, + // and then half a second later see if we got any more. Hopefully the half second delay is small + // enough not to annoy the user and large enough to account for noisy radio environments, but the + // numbers here are a heuristic, and may need to be tuned. If we have no recent data, we assume + // everything is ok, and declare the transmission complete. + + incomingTime = Date(timeIntervalSinceNow: 0) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Half second delay. + if self.incomingDelivered { + return + } + + let timeSinceLastData = -self.incomingTime.timeIntervalSinceNow // Make it positive. + let complete = timeSinceLastData > 0.25 + + if complete { + self.streamReceivedData(self.incomingData) + self.incomingDelivered = true + } + } + + if stream.hasBytesAvailable { + readBytes(from: stream) + } + } + + /// Methods to be overridden by child classes. + func streamIsOpen() { print("The stream is open.") } + func streamEnded() { print("The stream has ended.") } + func streamBytesAvailable() { print("The stream has bytes available.") } + func streamSpaceAvailable() { print("The stream has space available.") } + func streamError() { print("Stream error.") } + func streamUnknownEvent() { print("Stream unknown event.") } + func streamSentData(bytes _: Int, total: Int, fraction: Double) { print("Stream sent data.") } + func streamReceivedData(_: Data) { print("Stream received data.") } +} + +/// A UInt16 from Data extension. +extension UInt16 { + var data: Data { + var int = self + return Data(bytes: &int, count: MemoryLayout.size) + } +} + +/// A Data from UInt16 extension. +extension Data { + var uint16: UInt16 { + let i16array = withUnsafeBytes { $0.load(as: UInt16.self) } + return i16array + } +} + +/// A write() on OutputStream extension. +extension OutputStream { + func write(_ data: Data) -> Int { + return data.withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> Int in + let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self) + return self.write(bufferPointer.baseAddress!, maxLength: data.count) + } + } +} diff --git a/Sources/MobileSdk/MDocBLEUtils.swift b/Sources/MobileSdk/MDocBLEUtils.swift index cafc8ce..028919c 100644 --- a/Sources/MobileSdk/MDocBLEUtils.swift +++ b/Sources/MobileSdk/MDocBLEUtils.swift @@ -51,3 +51,68 @@ enum MDocReaderBLECallback { protocol MDocReaderBLEDelegate: AnyObject { func callback(message: MDocReaderBLECallback) } + +/// Return a string describing a BLE characteristic property. +func MDocCharacteristicPropertyName(_ prop: CBCharacteristicProperties) -> String { + return switch prop { + case .broadcast: "broadcast" + case .read: "read" + case .writeWithoutResponse: "write without response" + case .write: "write" + case .notify: "notify" + case .indicate: "indicate" + case .authenticatedSignedWrites: "authenticated signed writes" + case .extendedProperties: "extended properties" + case .notifyEncryptionRequired: "notify encryption required" + case .indicateEncryptionRequired: "indicate encryption required" + default: "unknown property" + } +} + +/// Return a string describing a BLE characteristic. +func MDocCharacteristicName(_ ch: CBCharacteristic) -> String { + return MDocCharacteristicNameFromUUID(ch.uuid) +} + +/// Return a string describing a BLE characteristic given its UUID. +func MDocCharacteristicNameFromUUID(_ ch: CBUUID) -> String { + return switch ch { + case holderStateCharacteristicId: "Holder:State" + case holderClient2ServerCharacteristicId: "Holder:Client2Server" + case holderServer2ClientCharacteristicId: "Holder:Server2Client" + case holderL2CAPCharacteristicId: "Holder:L2CAP" + + case readerStateCharacteristicId: "Reader:State" + case readerClient2ServerCharacteristicId: "Reader:Client2Server" + case readerServer2ClientCharacteristicId: "Reader:Server2Client" + case readerIdentCharacteristicId: "Reader:Ident" + case readerL2CAPCharacteristicId: "Reader:L2CAP" + + default: "Unknown:\(ch)" + } +} + +/// Print a description of a BLE characteristic. +func MDocDesribeCharacteristic(_ ch: CBCharacteristic) { + print(" \(MDocCharacteristicName(ch)) ( ", terminator:"") + + if(ch.properties.contains(.broadcast)) { print("broadcast", terminator:" ") } + if(ch.properties.contains(.read)) { print("read", terminator:" ") } + if(ch.properties.contains(.writeWithoutResponse)) { print("writeWithoutResponse", terminator:" ") } + if(ch.properties.contains(.write)) { print("write", terminator:" ") } + if(ch.properties.contains(.notify)) { print("notify", terminator:" ") } + if(ch.properties.contains(.indicate)) { print("indicate", terminator:" ") } + if(ch.properties.contains(.authenticatedSignedWrites)) { print("authenticatedSignedWrites", terminator:" ") } + if(ch.properties.contains(.extendedProperties)) { print("extendedProperties", terminator:" ") } + if(ch.properties.contains(.notifyEncryptionRequired)) { print("notifyEncryptionRequired", terminator:" ") } + if(ch.properties.contains(.indicateEncryptionRequired)) { print("indicateEncryptionRequired", terminator:" ") } + print(")") + + if let descriptors = ch.descriptors { + for d in descriptors { + print(" : \(d.uuid)") + } + } else { + print(" ") + } +} diff --git a/Sources/MobileSdk/MDocHolderBLECentral.swift b/Sources/MobileSdk/MDocHolderBLECentral.swift index 1c3b05c..31ba0be 100644 --- a/Sources/MobileSdk/MDocHolderBLECentral.swift +++ b/Sources/MobileSdk/MDocHolderBLECentral.swift @@ -4,11 +4,13 @@ import Foundation import os import SpruceIDMobileSdkRs +/// Characteristic errors. enum CharacteristicsError: Error { case missingMandatoryCharacteristic(name: String) case missingMandatoryProperty(name: String, characteristicName: String) } +/// Data errors. enum DataError: Error { case noData(characteristic: CBUUID) case invalidStateLength @@ -17,20 +19,45 @@ enum DataError: Error { case unknownDataTransferPrefix(byte: UInt8) } +/// The MDoc holder as a BLE central. class MDocHolderBLECentral: NSObject { + enum MachineState { + case initial, hardwareOn, fatalError, complete, halted + case awaitPeripheralDiscovery, peripheralDiscovered, checkPeripheral + case awaitRequest, requestReceived, sendingResponse + case l2capAwaitRequest, l2capRequestReceived, l2capSendingResponse + } + var centralManager: CBCentralManager! var serviceUuid: CBUUID var callback: MDocBLEDelegate var peripheral: CBPeripheral? + var writeCharacteristic: CBCharacteristic? var readCharacteristic: CBCharacteristic? var stateCharacteristic: CBCharacteristic? + var l2capCharacteristic: CBCharacteristic? + var maximumCharacteristicSize: Int? var writingQueueTotalChunks = 0 var writingQueueChunkIndex = 0 var writingQueue: IndexingIterator>? var incomingMessageBuffer = Data() + var outgoingMessageBuffer = Data() + + private var channelPSM: UInt16? = nil + private var activeStream: MDocHolderBLECentralConnection? + + /// If this is `false`, we decline to connect to L2CAP even if it is offered. + var useL2CAP = true + + var machineState = MachineState.initial + var machinePendingState = MachineState.initial { + didSet { + updateState() + } + } init(callback: MDocBLEDelegate, serviceUuid: CBUUID) { self.serviceUuid = serviceUuid @@ -39,10 +66,129 @@ class MDocHolderBLECentral: NSObject { self.centralManager = CBCentralManager(delegate: self, queue: nil) } - func startScanning() { - centralManager.scanForPeripherals(withServices: [serviceUuid]) - } + /// Update the state machine. + private func updateState() { + var update = true + + while update { + if machineState != machinePendingState { + print("「\(machineState) → \(machinePendingState)」") + } else { + print("「\(machineState)」") + } + + update = false + + switch machineState { + + /// Core. + case .initial: // Object just initialized, hardware not ready. + if machinePendingState == .hardwareOn { + machineState = machinePendingState + update = true + } + + case .hardwareOn: // Hardware is ready. + centralManager.scanForPeripherals(withServices: [serviceUuid]) + machineState = machinePendingState + machinePendingState = .awaitPeripheralDiscovery + break + + case .awaitPeripheralDiscovery: + if machinePendingState == .peripheralDiscovered { + machineState = machinePendingState + } + + case .peripheralDiscovered: + if machinePendingState == .checkPeripheral { + machineState = machinePendingState + + centralManager?.stopScan() + self.callback.callback(message: .connected) + } + + case .checkPeripheral: + if machinePendingState == .awaitRequest { + + if let peri = peripheral { + if useL2CAP, let l2capC = l2capCharacteristic { + peri.setNotifyValue(true, for: l2capC) + peri.readValue(for: l2capC) + machineState = .l2capAwaitRequest + } else if let readC = readCharacteristic, + let stateC = stateCharacteristic { + peri.setNotifyValue(true, for: readC) + peri.setNotifyValue(true, for: stateC) + peri.writeValue(_: Data([0x01]), for: stateC, type: .withoutResponse) + machineState = machinePendingState + } + } + } + + /// Original flow. + case .awaitRequest: + if machinePendingState == .requestReceived { + machineState = machinePendingState + callback.callback(message: MDocBLECallback.message(incomingMessageBuffer)) + incomingMessageBuffer = Data() + } + + /// The request has been received, we're waiting for the user to respond to the selective diclosure + /// dialog. + case .requestReceived: + if machinePendingState == .sendingResponse { + machineState = machinePendingState + let chunks = outgoingMessageBuffer.chunks(ofCount: maximumCharacteristicSize! - 1) + writingQueueTotalChunks = chunks.count + writingQueue = chunks.makeIterator() + writingQueueChunkIndex = 0 + drainWritingQueue() + update = true + } + + case .sendingResponse: + if machinePendingState == .complete { + machineState = machinePendingState + } + + /// L2CAP flow. + case .l2capAwaitRequest: + if machinePendingState == .l2capRequestReceived { + machineState = machinePendingState + callback.callback(message: MDocBLECallback.message(incomingMessageBuffer)) + incomingMessageBuffer = Data() + } + + /// The request has been received, we're waiting for the user to respond to the selective diclosure + /// dialog. + case .l2capRequestReceived: + if machinePendingState == .l2capSendingResponse { + machineState = machinePendingState + activeStream?.send(data: outgoingMessageBuffer) + machinePendingState = .l2capSendingResponse + update = true + } + + case .l2capSendingResponse: + if machinePendingState == .complete { + machineState = machinePendingState + } + + // + + case .fatalError: // Something went wrong. + machineState = .halted + machinePendingState = .halted + + case .complete: // Transfer complete. + break + case .halted: // Transfer incomplete, but we gave up. + break + } + } + } + func disconnectFromDevice () { let message: Data do { @@ -64,11 +210,17 @@ class MDocHolderBLECentral: NSObject { } func writeOutgoingValue(data: Data) { - let chunks = data.chunks(ofCount: maximumCharacteristicSize! - 1) - writingQueueTotalChunks = chunks.count - writingQueue = chunks.makeIterator() - writingQueueChunkIndex = 0 - drainWritingQueue() + outgoingMessageBuffer = data + switch machineState { + case .requestReceived: + machinePendingState = .sendingResponse + + case .l2capRequestReceived: + machinePendingState = .l2capSendingResponse + + default: + print("Unexpected write in state \(machineState)") + } } private func drainWritingQueue() { @@ -88,64 +240,71 @@ class MDocHolderBLECentral: NSObject { peripheral?.writeValue(_: chunk, for: writeCharacteristic!, type: CBCharacteristicWriteType.withoutResponse) + if firstByte == 0x00 { + machinePendingState = .complete + } } else { self.callback.callback(message: .uploadProgress(writingQueueTotalChunks, writingQueueTotalChunks)) writingQueue = nil + machinePendingState = .complete } } } - func processCharacteristics(peripheral: CBPeripheral, characteristics: [CBCharacteristic]) throws { - if let characteristic = characteristics.first(where: {$0.uuid == readerStateCharacteristicId}) { - if !characteristic.properties.contains(CBCharacteristicProperties.notify) { - throw CharacteristicsError.missingMandatoryProperty(name: "notify", characteristicName: "State") - } - if !characteristic.properties.contains(CBCharacteristicProperties.writeWithoutResponse) { - throw CharacteristicsError.missingMandatoryProperty( - name: "write without response", - characteristicName: "State" - ) - } - self.stateCharacteristic = characteristic - } else { - throw CharacteristicsError.missingMandatoryCharacteristic(name: "State") - } + /// Verify that a characteristic matches what is required of it. + private func getCharacteristic(list: [CBCharacteristic], uuid: CBUUID, properties: [CBCharacteristicProperties], required: Bool) throws -> CBCharacteristic? { + let chName = MDocCharacteristicNameFromUUID(uuid) - if let characteristic = characteristics.first(where: {$0.uuid == readerClient2ServerCharacteristicId}) { - if !characteristic.properties.contains(CBCharacteristicProperties.writeWithoutResponse) { - throw CharacteristicsError.missingMandatoryProperty( - name: "write without response", - characteristicName: "Client2Server" - ) + if let candidate = list.first(where: {$0.uuid == uuid}) { + for prop in properties { + if !candidate.properties.contains(prop) { + let propName = MDocCharacteristicPropertyName(prop) + if required { + throw CharacteristicsError.missingMandatoryProperty(name: propName, characteristicName: chName) + } else { + return nil + } + } } - self.writeCharacteristic = characteristic + return candidate } else { - throw CharacteristicsError.missingMandatoryCharacteristic(name: "Client2Server") - } - - if let characteristic = characteristics.first(where: {$0.uuid == readerServer2ClientCharacteristicId}) { - if !characteristic.properties.contains(CBCharacteristicProperties.notify) { - throw CharacteristicsError.missingMandatoryProperty(name: "notify", characteristicName: "Server2Client") + if required { + throw CharacteristicsError.missingMandatoryCharacteristic(name: chName) + } else { + return nil } - self.readCharacteristic = characteristic - } else { - throw CharacteristicsError.missingMandatoryCharacteristic(name: "Server2Client") } + } - if let characteristic = characteristics.first(where: {$0.uuid == readerIdentCharacteristicId}) { - if !characteristic.properties.contains(CBCharacteristicProperties.read) { - throw CharacteristicsError.missingMandatoryProperty(name: "read", characteristicName: "Ident") - } - peripheral.readValue(for: characteristic) - } else { - throw CharacteristicsError.missingMandatoryCharacteristic(name: "Ident") - } + /// Check that the reqiured characteristics are available with the required properties. + func processCharacteristics(peripheral: CBPeripheral, characteristics: [CBCharacteristic]) throws { - if let characteristic = characteristics.first(where: {$0.uuid == readerL2CAPCharacteristicId}) { - if !characteristic.properties.contains(CBCharacteristicProperties.read) { - throw CharacteristicsError.missingMandatoryProperty(name: "read", characteristicName: "L2CAP") - } + stateCharacteristic = try getCharacteristic(list: characteristics, + uuid: readerStateCharacteristicId, + properties: [.notify, .writeWithoutResponse], + required: true) + + writeCharacteristic = try getCharacteristic(list: characteristics, + uuid: readerClient2ServerCharacteristicId, + properties: [.writeWithoutResponse], + required: true) + + readCharacteristic = try getCharacteristic(list: characteristics, + uuid: readerServer2ClientCharacteristicId, + properties: [.notify], + required: true) + + if let readerIdent = try getCharacteristic(list: characteristics, + uuid: readerIdentCharacteristicId, + properties: [.read], + required: true) { + peripheral.readValue(for: readerIdent) } + + l2capCharacteristic = try getCharacteristic(list: characteristics, + uuid: readerL2CAPCharacteristicId, + properties: [.read], + required: false) // iOS controls MTU negotiation. Since MTU is just a maximum, we can use a lower value than the negotiated value. // 18013-5 expects an upper limit of 515 MTU, so we cap at this even if iOS negotiates a higher value. @@ -156,10 +315,14 @@ class MDocHolderBLECentral: NSObject { } + /// Process incoming data from a peripheral. This handles incoming data from any and all characteristics (though not + /// the L2CAP stream...), so we hit this call multiple times from several angles, at least in the original flow. func processData(peripheral: CBPeripheral, characteristic: CBCharacteristic) throws { if var data = characteristic.value { - print("Processing data for \(characteristic.uuid)") + print("Processing \(data.count) bytes for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") switch characteristic.uuid { + + /// Transfer indicator. case readerStateCharacteristicId: if data.count != 1 { throw DataError.invalidStateLength @@ -171,34 +334,47 @@ class MDocHolderBLECentral: NSObject { case let byte: throw DataError.unknownState(byte: byte) } + + /// Incoming request. case readerServer2ClientCharacteristicId: 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: MDocBLECallback.message(incomingMessageBuffer)) - self.incomingMessageBuffer = Data() - return + print("End") + machinePendingState = .requestReceived + case 0x01: // partial - print("Partial message") + print("Chunk") // TODO check length against MTU - return + case let .some(byte): throw DataError.unknownDataTransferPrefix(byte: byte) } - // Looks like this should just happen after discovering characteristics + + /// Ident check. case readerIdentCharacteristicId: - self.peripheral?.setNotifyValue(true, for: self.readCharacteristic!) - self.peripheral?.setNotifyValue(true, for: self.stateCharacteristic!) - self.peripheral?.writeValue(_: Data([0x01]), - for: self.stateCharacteristic!, - type: CBCharacteristicWriteType.withoutResponse) - return + // Looks like this should just happen after discovering characteristics + print("Ident") + // TODO: Presumably we should be doing something with the ident value; probably handing it + // to the callback to see if the caller likes it. + machinePendingState = .awaitRequest + + /// L2CAP channel ID. case readerL2CAPCharacteristicId: + print("PSM: ", terminator: "") + if data.count == 2 { + let psm = data.uint16 + print("\(psm)") + channelPSM = psm + peripheral.openL2CAPChannel(psm) + machinePendingState = .l2capAwaitRequest + } return + case let uuid: throw DataError.unknownCharacteristic(uuid: uuid) } @@ -209,13 +385,17 @@ class MDocHolderBLECentral: NSObject { } extension MDocHolderBLECentral: CBCentralManagerDelegate { + + /// Handle a state change in the central manager. func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == .poweredOn { - startScanning() + machinePendingState = .hardwareOn } else { self.callback.callback(message: .error(.bluetooth(central))) } } + + /// Handle discovering a peripheral. func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], @@ -224,16 +404,19 @@ extension MDocHolderBLECentral: CBCentralManagerDelegate { peripheral.delegate = self self.peripheral = peripheral centralManager?.connect(peripheral, options: nil) + machinePendingState = .peripheralDiscovered } + + /// Handle connecting to a peripheral. func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - print("Connected to peripheral") - centralManager?.stopScan() peripheral.discoverServices([self.serviceUuid]) - self.callback.callback(message: .connected) + machinePendingState = .checkPeripheral } } extension MDocHolderBLECentral: CBPeripheralDelegate { + + /// Handle discovery of peripheral services. func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if (error) != nil { self.callback.callback( @@ -249,6 +432,7 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { } } + /// Handle discovery of characteristics for a peripheral service. func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if (error) != nil { self.callback.callback( @@ -267,9 +451,9 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { } } + /// Handle a characteristic value being updated. func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { do { - print("Processing data") try self.processData(peripheral: peripheral, characteristic: characteristic) } catch { self.callback.callback(message: .error(.peripheral("\(error)"))) @@ -284,9 +468,22 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { drainWritingQueue() } + + func peripheral(_: CBPeripheral, didOpen channel: CBL2CAPChannel?, error: Error?) { + if let error = error { + print("Error opening l2cap channel - \(error.localizedDescription)") + return + } + + if let channel = channel { + activeStream = MDocHolderBLECentralConnection(delegate: self, channel: channel) + } + } } extension MDocHolderBLECentral: CBPeripheralManagerDelegate { + + /// Handle peripheral manager state change. func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { switch peripheral.state { case .poweredOn: @@ -306,3 +503,22 @@ extension MDocHolderBLECentral: CBPeripheralManagerDelegate { } } } + +extension MDocHolderBLECentral: MDocHolderBLECentralConnectionDelegate { + + func request(_ data: Data) { + incomingMessageBuffer = data + machinePendingState = .l2capRequestReceived + } + + func sendUpdate(bytes: Int, total: Int, fraction: Double) { + self.callback.callback(message: .uploadProgress(bytes, total)) + } + + func sendComplete() { + machinePendingState = .complete + } + + func connectionEnd() { + } +} diff --git a/Sources/MobileSdk/MDocHolderBLECentralConnection.swift b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift new file mode 100644 index 0000000..4663b74 --- /dev/null +++ b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift @@ -0,0 +1,68 @@ +// Derived from MIT-licensed work by Paul Wilkinson: https://github.com/paulw11/L2Cap + +import CoreBluetooth +import Foundation + +public protocol MDocHolderBLECentralConnectionDelegate { + func request(_ data: Data) + func sendUpdate(bytes: Int, total: Int, fraction: Double) + func sendComplete() + func connectionEnd() +} + +class MDocHolderBLECentralConnection: BLEInternalConnection { + private let controlDelegate: MDocHolderBLECentralConnectionDelegate + + /// Initialize a reader peripheral connection. + init(delegate: MDocHolderBLECentralConnectionDelegate, channel: CBL2CAPChannel) { + controlDelegate = delegate + super.init() + self.channel = channel + channel.inputStream.delegate = self + channel.outputStream.delegate = self + channel.inputStream.schedule(in: RunLoop.main, forMode: .default) + channel.outputStream.schedule(in: RunLoop.main, forMode: .default) + channel.inputStream.open() + channel.outputStream.open() + } + + /// Called by super when the stream is open. + override func streamIsOpen() {} + + /// Called by super when the stream ends. + override func streamEnded() { + close() + controlDelegate.connectionEnd() + } + + /// Called by super when the stream has readable data. + override func streamBytesAvailable() {} + + /// Called by super when the stream has space in the outbound buffer. + override func streamSpaceAvailable() {} + + /// Called by super if the stream encounters an error. + override func streamError() { + close() + controlDelegate.connectionEnd() + } + + /// Called by super if an unknown stream event occurs. + override func streamUnknownEvent() {} + + /// Called by super when data is sent. + override func streamSentData(bytes: Int, total: Int, fraction: Double) { + print("Stream sent \(bytes) of \(total) bytes, \(fraction * 100)% complete.") + + controlDelegate.sendUpdate(bytes: bytes, total: total, fraction: fraction) + + if bytes == total { + controlDelegate.sendComplete() + } + } + + /// Called by super when data is received. + override func streamReceivedData(_ data: Data) { + controlDelegate.request(data) + } +} diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index 7e55e46..d8704a8 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -3,7 +3,19 @@ import CoreBluetooth import Foundation import SpruceIDMobileSdkRs +// NOTE: https://blog.valerauko.net/2024/03/24/some-notes-on-ios-ble/ +// error 431 is "peer requested disconnect" +// error 436 is "local requested disconnect" + class MDocReaderBLEPeripheral: NSObject { + enum MachineState { + case initial, hardwareOn, servicePublished + case fatalError, complete, halted + case l2capRead, l2capAwaitChannelPublished, l2capChannelPublished + case l2capStreamOpen, l2capSendingRequest, l2capAwaitingResponse + case stateSubscribed, awaitRequestStart, sendingRequest, awaitResponse + } + var peripheralManager: CBPeripheralManager! var serviceUuid: CBUUID var bleIdent: Data @@ -21,6 +33,25 @@ class MDocReaderBLEPeripheral: NSObject { var writingQueueChunkIndex: Int? var writingQueue: IndexingIterator>? + var activeStream: MDocReaderBLEPeripheralConnection? + + /// If this is `true`, we offer an L2CAP characteristic and set up an L2CAP stream. If it is `false` we do neither + /// of these things, and use the old flow. + var useL2CAP = true + + private var channelPSM: UInt16? = nil { + didSet { + updatePSM() + } + } + + var machineState = MachineState.initial + var machinePendingState = MachineState.initial { + didSet { + updateState() + } + } + init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data) { self.serviceUuid = serviceUuid self.callback = callback @@ -31,6 +62,130 @@ class MDocReaderBLEPeripheral: NSObject { self.peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) } + /// Update the state machine. + private func updateState() { + var update = true + + while update { + if machineState != machinePendingState { + print("「\(machineState) → \(machinePendingState)」") + } else { + print("「\(machineState)」") + } + + update = false + + switch machineState { + + /// Core. + case .initial: // Object just initialized, hardware not ready. + if machinePendingState == .hardwareOn { + machineState = .hardwareOn + update = true + } + + case .hardwareOn: // Hardware is ready. + print("Advertising...") + setupService() + peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [serviceUuid]]) + machineState = .servicePublished + machinePendingState = .servicePublished + update = true + + case .servicePublished: // Characteristics set up, we're publishing our service. + if machinePendingState == .l2capRead { + machineState = machinePendingState + update = true + } else if machinePendingState == .stateSubscribed { + machineState = machinePendingState + update = true + } + break + + case .fatalError: // Something went wrong. + machineState = .halted + machinePendingState = .halted + + case .complete: // Transfer complete. + break + + case .halted: // Transfer incomplete, but we gave up. + break + + /// L2CAP flow. + case .l2capRead: // We have a read on our L2CAP characteristic, start L2CAP flow. + machineState = .l2capAwaitChannelPublished + peripheralManager.publishL2CAPChannel(withEncryption: true) + update = true + + case .l2capAwaitChannelPublished: + if machinePendingState == .l2capChannelPublished { + machineState = machinePendingState + } + + case .l2capChannelPublished: + if machinePendingState == .l2capStreamOpen { + machineState = machinePendingState + update = true + } + + case .l2capStreamOpen: // An L2CAP stream is opened. + activeStream?.send(data: requestData) + machineState = .l2capSendingRequest + machinePendingState = .l2capSendingRequest + update = true + + case .l2capSendingRequest: // The request is being sent over the L2CAP stream. + if machinePendingState == .l2capAwaitingResponse { + machineState = machinePendingState + update = true + } + break + + case .l2capAwaitingResponse: // The request is sent, the response is (hopefully) coming in. + if machinePendingState == .complete { + machineState = machinePendingState + callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) + update = true + } + + /// Original flow. + case .stateSubscribed: // We have a subscription to our State characteristic, start original flow. + // This will trigger wallet-sdk-swift to send 0x01 to start the exchange + peripheralManager.updateValue(bleIdent, for: identCharacteristic!, onSubscribedCentrals: nil) + + // I think the updateValue() below is out of spec; 8.3.3.1.1.5 says we wait for a write without + // response of 0x01 to State, but that's supposed to come from the holder to indicate it's ready + // for us to initiate. + + // This will trigger wallet-sdk-kt to send 0x01 to start the exchange + //peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + + machineState = .awaitRequestStart + machinePendingState = .awaitRequestStart + + case .awaitRequestStart: // We've let the holder know we're ready, waiting for their ack. + if machinePendingState == .sendingRequest { + writeOutgoingValue(data: requestData) + machineState = .sendingRequest + } + + case .sendingRequest: + if machinePendingState == .awaitResponse { + machineState = .awaitResponse + } + break + + case .awaitResponse: + if machinePendingState == .complete { + machineState = .complete + update = true + } + break + } + } + } + func setupService() { let service = CBMutableService(type: self.serviceUuid, primary: true) // CBUUIDClientCharacteristicConfigurationString only returns "2902" @@ -59,17 +214,30 @@ class MDocReaderBLEPeripheral: NSObject { 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 - ] + if useL2CAP { + // 18013-5 doesn't require .indicate, but without it we don't seem to be able to propagate the PSM + // through to central. + self.l2capCharacteristic = CBMutableCharacteristic(type: readerL2CAPCharacteristicId, + properties: [.read, .indicate], + value: nil, + permissions: [.readable]) + + if let stateC = stateCharacteristic, + let readC = readCharacteristic, + let writeC = writeCharacteristic, + let identC = identCharacteristic, + let l2capC = l2capCharacteristic { + + service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC, l2capC] + } + } else { + if let stateC = stateCharacteristic, + let readC = readCharacteristic, + let writeC = writeCharacteristic, + let identC = identCharacteristic { + service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC] + } + } peripheralManager.add(service) } @@ -77,6 +245,7 @@ class MDocReaderBLEPeripheral: NSObject { return } + /// Write the request using the old flow. func writeOutgoingValue(data: Data) { let chunks = data.chunks(ofCount: maximumCharacteristicSize! - 1) writingQueueTotalChunks = chunks.count @@ -99,67 +268,96 @@ class MDocReaderBLEPeripheral: NSObject { chunk.append(firstByte) chunk.reverse() self.peripheralManager?.updateValue(chunk, for: self.writeCharacteristic!, onSubscribedCentrals: nil) + + if firstByte == 0x00 { + machinePendingState = .awaitResponse + } } else { writingQueue = nil + machinePendingState = .awaitResponse } } } + /// Process incoming data. func processData(central: CBCentral, characteristic: CBCharacteristic, value: Data?) throws { if var data = value { - print("Processing data for \(characteristic.uuid)") + print("Processing \(data.count) bytes of data for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") switch characteristic.uuid { + case readerClient2ServerCharacteristicId: let firstByte = data.popFirst() incomingMessageBuffer.append(data) switch firstByte { case .none: + print("Nothing?") throw DataError.noData(characteristic: characteristic.uuid) case 0x00: // end - print("End of message") + print("End") self.callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) self.incomingMessageBuffer = Data() self.incomingMessageIndex = 0 + machinePendingState = .complete return case 0x01: // partial - print("Partial message") + print("Chunk") self.incomingMessageIndex += 1 self.callback.callback(message: .downloadProgress(self.incomingMessageIndex)) // TODO check length against MTU return case let .some(byte): + print("Unexpected byte \(String(format: "$%02X", byte))") throw DataError.unknownDataTransferPrefix(byte: byte) } - case readerStateCharacteristicId: + + case readerStateCharacteristicId: + print("State") if data.count != 1 { throw DataError.invalidStateLength } switch data[0] { case 0x01: - print("Starting to send request") - writeOutgoingValue(data: self.requestData) + machinePendingState = .sendingRequest case let byte: throw DataError.unknownState(byte: byte) } - return -// case readerL2CAPCharacteristicId: -// return - case let uuid: + + case readerL2CAPCharacteristicId: + print("L2CAP") + machinePendingState = .l2capRead + return + + case let uuid: + print("Unexpected UUID") throw DataError.unknownCharacteristic(uuid: uuid) } } else { throw DataError.noData(characteristic: characteristic.uuid) } } + + /// Update the channel PSM. + private func updatePSM() { + l2capCharacteristic?.value = channelPSM?.data + + if let l2capC = l2capCharacteristic { + let value = channelPSM?.data ?? Data() + + l2capC.value = value + print("Sending l2cap channel update \(value.uint16).") + peripheralManager.updateValue(value, for: l2capC, onSubscribedCentrals: nil) + } + } } +/// Peripheral manager delegate functions. extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { + + /// Handle the peripheral updating state. func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { switch peripheral.state { case .poweredOn: - print("Advertising...") - setupService() - peripheralManager.startAdvertising([CBAdvertisementDataServiceUUIDsKey: [serviceUuid]]) + machinePendingState = .hardwareOn case .unsupported: print("Peripheral Is Unsupported.") case .unauthorized: @@ -175,28 +373,42 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } } - // This is called when there is space in the queue again (so it is part of the loop for drainWritingQueue) + /// Handle space available for sending. This is part of the send loop for the old (non-L2CAP) flow. func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { self.drainWritingQueue() } + /// Handle incoming subscriptions. func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { - print("Subscribed to \(characteristic.uuid)") + print("Subscribed to \(MDocCharacteristicNameFromUUID(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 l2capCharacteristic: // If we get this, we're in the L2CAP flow. + // TODO: If this gets hit after a subscription to the State characteristic, something has gone wrong; + // the holder should choose one flow or the other. We have options here: + // + // - ignore the corner case -- what the code is doing now, not ideal + // - error out -- the holder is doing something screwy, we want no part of it + // - try to adapt -- send the data a second time, listen on both L2CAP and normal - probably a bad idea; + // it will make us mildly more tolerant of out-of-spec holders, but may increase our attack surface + machinePendingState = .l2capRead + break + + case readerStateCharacteristicId: // If we get this, we're in the original flow. + // TODO: See the comment block in the L2CAP characteristic, above; only one of these can be valid for + // a given exchange. + + machinePendingState = .stateSubscribed + case _: return } } - + + /// Handle read requests. func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { - print("Received read request for \(request.characteristic.uuid)") + print("Received read request for \(MDocCharacteristicNameFromUUID(request.characteristic.uuid))") // Since there is no callback for MTU on iOS we will grab it here. maximumCharacteristicSize = min(request.central.maximumUpdateValueLength, 512) @@ -204,20 +416,20 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { 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) + peripheralManager.respond(to: request, withResult: .success) + machinePendingState = .l2capRead } else { - self.callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(request.characteristic.uuid)"))) + self.callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(MDocCharacteristicNameFromUUID(request.characteristic.uuid))"))) } } + /// Handle write requests. 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) { @@ -229,4 +441,45 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } } } + + /// Handle an L2CAP channel being published. + public func peripheralManager(_: CBPeripheralManager, didPublishL2CAPChannel PSM: CBL2CAPPSM, error: Error?) { + if let error = error { + print("Error publishing channel: \(error.localizedDescription)") + return + } + print("Published channel \(PSM)") + channelPSM = PSM + machinePendingState = .l2capChannelPublished + } + + /// Handle an L2CAP channel opening. + public func peripheralManager(_: CBPeripheralManager, didOpen channel: CBL2CAPChannel?, error: Error?) { + if let error = error { + print("Error opening channel: \(error.localizedDescription)") + return + } + + if let channel = channel { + activeStream = MDocReaderBLEPeripheralConnection(delegate: self, channel: channel) + } + } +} + +/// L2CAP Stream delegate functions. +extension MDocReaderBLEPeripheral: MDocReaderBLEPeripheralConnectionDelegate { + func streamOpen() { + machinePendingState = .l2capStreamOpen + } + + func sentData(_ bytes: Int) { + if bytes >= requestData.count { + machinePendingState = .l2capAwaitingResponse + } + } + + func receivedData(_ data: Data) { + incomingMessageBuffer = data + machinePendingState = .complete + } } diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift new file mode 100644 index 0000000..69d1fbd --- /dev/null +++ b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift @@ -0,0 +1,62 @@ +// Derived from MIT-licensed work by Paul Wilkinson: https://github.com/paulw11/L2Cap + +import CoreBluetooth +import Foundation + +protocol MDocReaderBLEPeripheralConnectionDelegate { + func streamOpen() + func sentData(_ bytes: Int) + func receivedData(_ data: Data) +} + +class MDocReaderBLEPeripheralConnection: BLEInternalConnection { + private let controlDelegate: MDocReaderBLEPeripheralConnectionDelegate + + /// Initialize a reader peripheral connection. + init(delegate: MDocReaderBLEPeripheralConnectionDelegate, channel: CBL2CAPChannel) { + controlDelegate = delegate + super.init() + self.channel = channel + channel.inputStream.delegate = self + channel.outputStream.delegate = self + channel.inputStream.schedule(in: RunLoop.main, forMode: .default) + channel.outputStream.schedule(in: RunLoop.main, forMode: .default) + channel.inputStream.open() + channel.outputStream.open() + } + + /// Called by super when the stream opens. + override func streamIsOpen() { + controlDelegate.streamOpen() + } + + /// Called by super when the stream ends. + override func streamEnded() { + close() + } + + /// Called by super when the stream has bytes available for reading. + override func streamBytesAvailable() {} + + /// Called by super when the stream has buffer space available for sending. + override func streamSpaceAvailable() {} + + /// Called by super when the stream has an error. + override func streamError() { + close() + } + + /// Called by super when the stream has an unknown event; these can probably be ignored. + override func streamUnknownEvent() {} + + /// Called by super when data is sent. + override func streamSentData(bytes: Int, total: Int, fraction: Double) { + print("Stream sent \(bytes) of \(total) bytes, \(fraction * 100)% complete.") + controlDelegate.sentData(bytes) + } + + /// Called by super when data is received. + override func streamReceivedData(_ data: Data) { + controlDelegate.receivedData(data) + } +} From de1e035f282282a67c7106e3352c64c1206bf64b Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Thu, 5 Sep 2024 01:15:54 -0400 Subject: [PATCH 11/17] Feeding everything to the formatter. --- Sources/MobileSdk/BLEConnection.swift | 2 +- Sources/MobileSdk/Credential.swift | 2 +- Sources/MobileSdk/CredentialPack.swift | 23 ++- Sources/MobileSdk/Credentials.swift | 4 +- Sources/MobileSdk/DataConversions.swift | 16 +- Sources/MobileSdk/GenericJSON.swift | 25 +-- Sources/MobileSdk/KeyManager.swift | 94 +++++------ Sources/MobileSdk/MDoc.swift | 73 ++++----- Sources/MobileSdk/MDocBLEUtils.swift | 38 +++-- Sources/MobileSdk/MDocHolderBLECentral.swift | 116 +++++++------- Sources/MobileSdk/MDocReader.swift | 36 ++--- .../MobileSdk/MDocReaderBLEPeripheral.swift | 151 ++++++++---------- Sources/MobileSdk/PlatformContext.swift | 4 +- Sources/MobileSdk/StorageManager.swift | 6 +- Sources/MobileSdk/W3CVC.swift | 5 +- 15 files changed, 289 insertions(+), 306 deletions(-) diff --git a/Sources/MobileSdk/BLEConnection.swift b/Sources/MobileSdk/BLEConnection.swift index 0d0e09b..2575c17 100644 --- a/Sources/MobileSdk/BLEConnection.swift +++ b/Sources/MobileSdk/BLEConnection.swift @@ -147,7 +147,7 @@ class BLEInternalConnection: NSObject, StreamDelegate { func streamSpaceAvailable() { print("The stream has space available.") } func streamError() { print("Stream error.") } func streamUnknownEvent() { print("Stream unknown event.") } - func streamSentData(bytes _: Int, total: Int, fraction: Double) { print("Stream sent data.") } + func streamSentData(bytes _: Int, total _: Int, fraction _: Double) { print("Stream sent data.") } func streamReceivedData(_: Data) { print("Stream received data.") } } diff --git a/Sources/MobileSdk/Credential.swift b/Sources/MobileSdk/Credential.swift index 54980e9..75f6799 100644 --- a/Sources/MobileSdk/Credential.swift +++ b/Sources/MobileSdk/Credential.swift @@ -9,7 +9,7 @@ open class Credential: Identifiable { open func get(keys: [String]) -> [String: GenericJSON] { if keys.contains("id") { - return ["id": GenericJSON.string(self.id)] + return ["id": GenericJSON.string(id)] } else { return [:] } diff --git a/Sources/MobileSdk/CredentialPack.swift b/Sources/MobileSdk/CredentialPack.swift index 6bc6449..9a44133 100644 --- a/Sources/MobileSdk/CredentialPack.swift +++ b/Sources/MobileSdk/CredentialPack.swift @@ -1,12 +1,11 @@ -import Foundation import CryptoKit +import Foundation public class CredentialPack { - private var credentials: [Credential] public init() { - self.credentials = [] + credentials = [] } public init(credentials: [Credential]) { @@ -16,8 +15,8 @@ public class CredentialPack { public func addW3CVC(credentialString: String) throws -> [Credential]? { do { let credential = try W3CVC(credentialString: credentialString) - self.credentials.append(credential) - return self.credentials + credentials.append(credential) + return credentials } catch { throw error } @@ -26,13 +25,13 @@ public class CredentialPack { public func addMDoc(mdocBase64: String, keyAlias: String = UUID().uuidString) throws -> [Credential]? { let mdocData = Data(base64Encoded: mdocBase64)! let credential = MDoc(fromMDoc: mdocData, namespaces: [:], keyAlias: keyAlias)! - self.credentials.append(credential) - return self.credentials + credentials.append(credential) + return credentials } public func get(keys: [String]) -> [String: [String: GenericJSON]] { var values: [String: [String: GenericJSON]] = [:] - for cred in self.credentials { + for cred in credentials { values[cred.id] = cred.get(keys: keys) } @@ -40,14 +39,14 @@ public class CredentialPack { } public func get(credentialsIds: [String]) -> [Credential] { - return self.credentials.filter { credentialsIds.contains($0.id) } + return credentials.filter { credentialsIds.contains($0.id) } } public func get(credentialId: String) -> Credential? { - if let credential = self.credentials.first(where: { $0.id == credentialId }) { - return credential + if let credential = credentials.first(where: { $0.id == credentialId }) { + return credential } else { - return nil + return nil } } } diff --git a/Sources/MobileSdk/Credentials.swift b/Sources/MobileSdk/Credentials.swift index 3fd16dd..99e37be 100644 --- a/Sources/MobileSdk/Credentials.swift +++ b/Sources/MobileSdk/Credentials.swift @@ -8,11 +8,11 @@ public class CredentialStore { } // swiftlint:disable force_cast - public func presentMdocBLE(deviceEngagement: DeviceEngagement, + public func presentMdocBLE(deviceEngagement _: DeviceEngagement, callback: BLESessionStateDelegate // , trustedReaders: TrustedReaders ) -> BLESessionManager? { - if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) { + if let firstMdoc = credentials.first(where: { $0 is MDoc }) { return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback) } else { return nil diff --git a/Sources/MobileSdk/DataConversions.swift b/Sources/MobileSdk/DataConversions.swift index 73d3e9a..0d7cefc 100644 --- a/Sources/MobileSdk/DataConversions.swift +++ b/Sources/MobileSdk/DataConversions.swift @@ -1,13 +1,13 @@ import Foundation extension Data { - var base64EncodedUrlSafe: String { - let string = self.base64EncodedString() + var base64EncodedUrlSafe: String { + let string = base64EncodedString() - // Make this URL safe and remove padding - return string - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } + // Make this URL safe and remove padding + return string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } } diff --git a/Sources/MobileSdk/GenericJSON.swift b/Sources/MobileSdk/GenericJSON.swift index fb1d859..6e40c2d 100644 --- a/Sources/MobileSdk/GenericJSON.swift +++ b/Sources/MobileSdk/GenericJSON.swift @@ -31,11 +31,11 @@ extension GenericJSON: Codable { public func toString() -> String { switch self { - case .string(let str): + case let .string(str): return str - case .number(let num): + case let .number(num): return num.debugDescription - case .bool(let bool): + case let .bool(bool): return bool.description case .null: return "null" @@ -71,11 +71,11 @@ extension GenericJSON: Codable { extension GenericJSON: CustomDebugStringConvertible { public var debugDescription: String { switch self { - case .string(let str): + case let .string(str): return str.debugDescription - case .number(let num): + case let .number(num): return num.debugDescription - case .bool(let bool): + case let .bool(bool): return bool.description case .null: return "null" @@ -89,26 +89,28 @@ extension GenericJSON: CustomDebugStringConvertible { public extension GenericJSON { var dictValue: [String: GenericJSON]? { - if case .object(let value) = self { + if case let .object(value) = self { return value } return nil } + var arrayValue: [GenericJSON]? { - if case .array(let value) = self { + if case let .array(value) = self { return value } return nil } + subscript(index: Int) -> GenericJSON? { - if case .array(let arr) = self, arr.indices.contains(index) { + if case let .array(arr) = self, arr.indices.contains(index) { return arr[index] } return nil } subscript(key: String) -> GenericJSON? { - if case .object(let dict) = self { + if case let .object(dict) = self { return dict[key] } return nil @@ -123,7 +125,7 @@ public extension GenericJSON { } func queryKeyPath(_ path: T) -> GenericJSON? where T: Collection, T.Element == String { - guard case .object(let object) = self else { + guard case let .object(object) = self else { return nil } guard let head = path.first else { @@ -135,5 +137,4 @@ public extension GenericJSON { let tail = path.dropFirst() return tail.isEmpty ? value : value.queryKeyPath(tail) } - } diff --git a/Sources/MobileSdk/KeyManager.swift b/Sources/MobileSdk/KeyManager.swift index a62a687..7c276ce 100644 --- a/Sources/MobileSdk/KeyManager.swift +++ b/Sources/MobileSdk/KeyManager.swift @@ -8,7 +8,7 @@ public class KeyManager: NSObject { */ public static func reset() -> Bool { let query: [String: Any] = [ - kSecClass as String: kSecClassKey + kSecClass as String: kSecClassKey, ] let ret = SecItemDelete(query as CFDictionary) @@ -24,7 +24,7 @@ public class KeyManager: NSObject { kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: tag, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true + kSecReturnRef as String: true, ] var item: CFTypeRef? @@ -36,24 +36,24 @@ public class KeyManager: NSObject { * Returns a secret key - based on the id of the key. */ public static func getSecretKey(id: String) -> SecKey? { - let tag = id.data(using: .utf8)! - let query: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: tag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true - ] + let tag = id.data(using: .utf8)! + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true, + ] - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) - guard status == errSecSuccess else { return nil } + guard status == errSecSuccess else { return nil } - // swiftlint:disable force_cast - let key = item as! SecKey - // swiftlint:enable force_cast + // swiftlint:disable force_cast + let key = item as! SecKey + // swiftlint:enable force_cast - return key + return key } /** @@ -66,7 +66,8 @@ public class KeyManager: NSObject { kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, - nil)! + nil + )! let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, @@ -76,13 +77,13 @@ public class KeyManager: NSObject { kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true, kSecAttrApplicationTag as String: tag, - kSecAttrAccessControl as String: access - ] + kSecAttrAccessControl as String: access, + ], ] var error: Unmanaged? SecKeyCreateRandomKey(attributes as CFDictionary, &error) - if error != nil { print(error!) } + if error != nil { print(error!) } return error == nil } @@ -90,35 +91,35 @@ public class KeyManager: NSObject { * Returns a JWK for a particular secret key by key id. */ public static func getJwk(id: String) -> String? { - guard let key = getSecretKey(id: id) else { return nil } + guard let key = getSecretKey(id: id) else { return nil } - guard let publicKey = SecKeyCopyPublicKey(key) else { - return nil - } + guard let publicKey = SecKeyCopyPublicKey(key) else { + return nil + } - var error: Unmanaged? - guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else { - return nil - } + var error: Unmanaged? + guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else { + return nil + } - let fullData: Data = data.subdata(in: 1..? diff --git a/Sources/MobileSdk/MDoc.swift b/Sources/MobileSdk/MDoc.swift index 4c8c736..f6a6a46 100644 --- a/Sources/MobileSdk/MDoc.swift +++ b/Sources/MobileSdk/MDoc.swift @@ -15,10 +15,10 @@ public class MDoc: Credential { /// namespaces is the full set of namespaces with data items and their value /// IssuerSignedItemBytes will be bytes, but its composition is defined here /// https://github.com/spruceid/isomdl/blob/f7b05dfa/src/definitions/issuer_signed.rs#L18 - public init?(fromMDoc issuerAuth: Data, namespaces: [MDocNamespace: [IssuerSignedItemBytes]], keyAlias: String) { + public init?(fromMDoc issuerAuth: Data, namespaces _: [MDocNamespace: [IssuerSignedItemBytes]], keyAlias: String) { self.keyAlias = keyAlias do { - try self.inner = SpruceIDMobileSdkRs.MDoc.fromCbor(value: issuerAuth) + try inner = SpruceIDMobileSdkRs.MDoc.fromCbor(value: issuerAuth) } catch { print("\(error)") return nil @@ -44,15 +44,15 @@ public class BLESessionManager { var mdoc: MDoc var bleManager: MDocHolderBLECentral! - init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate) { + init?(mdoc: MDoc, engagement _: DeviceEngagement, callback: BLESessionStateDelegate) { self.callback = callback - self.uuid = UUID() + uuid = UUID() self.mdoc = mdoc do { let sessionData = try SpruceIDMobileSdkRs.initialiseSession(document: mdoc.inner, - uuid: self.uuid.uuidString) - self.state = sessionData.state - bleManager = MDocHolderBLECentral(callback: self, serviceUuid: CBUUID(nsuuid: self.uuid)) + uuid: uuid.uuidString) + state = sessionData.state + bleManager = MDocHolderBLECentral(callback: self, serviceUuid: CBUUID(nsuuid: uuid)) self.callback.update(state: .engagingQRCode(sessionData.qrCodeUri.data(using: .ascii)!)) } catch { print("\(error)") @@ -70,8 +70,8 @@ public class BLESessionManager { let payload = try SpruceIDMobileSdkRs.submitResponse(sessionManager: sessionManager!, permittedItems: items) let query = [kSecClass: kSecClassKey, - kSecAttrApplicationLabel: self.mdoc.keyAlias, - kSecReturnRef: true] as [String: Any] + kSecAttrApplicationLabel: mdoc.keyAlias, + kSecReturnRef: true] as [String: Any] // Find and cast the result as a SecKey instance. var item: CFTypeRef? @@ -80,31 +80,32 @@ public class BLESessionManager { case errSecSuccess: // swiftlint:disable force_cast secKey = item as! SecKey - // swiftlint:enable force_cast + // swiftlint:enable force_cast case errSecItemNotFound: - self.callback.update(state: .error(.generic("Key not found"))) - self.cancel() + callback.update(state: .error(.generic("Key not found"))) + cancel() return case let status: - self.callback.update(state: .error(.generic("Keychain read failed: \(status)"))) - self.cancel() + callback.update(state: .error(.generic("Keychain read failed: \(status)"))) + cancel() return } var error: Unmanaged? guard let derSignature = SecKeyCreateSignature(secKey, .ecdsaSignatureMessageX962SHA256, payload as CFData, - &error) as Data? else { - self.callback.update(state: .error(.generic("Failed to sign message: \(error.debugDescription)"))) - self.cancel() + &error) as Data? + else { + callback.update(state: .error(.generic("Failed to sign message: \(error.debugDescription)"))) + cancel() return } let response = try SpruceIDMobileSdkRs.submitSignature(sessionManager: sessionManager!, - derSignature: derSignature) - self.bleManager.writeOutgoingValue(data: response) + derSignature: derSignature) + bleManager.writeOutgoingValue(data: response) } catch { - self.callback.update(state: .error(.generic("\(error)"))) - self.cancel() + callback.update(state: .error(.generic("\(error)"))) + cancel() } } } @@ -113,23 +114,23 @@ extension BLESessionManager: MDocBLEDelegate { func callback(message: MDocBLECallback) { switch message { case .done: - self.callback.update(state: .success) + callback.update(state: .success) case .connected: - self.callback.update(state: .connected) - case .uploadProgress(let value, let total): - self.callback.update(state: .uploadProgress(value, total)) - case .message(let data): + callback.update(state: .connected) + case let .uploadProgress(value, total): + callback.update(state: .uploadProgress(value, total)) + case let .message(data): do { - let requestData = try SpruceIDMobileSdkRs.handleRequest(state: self.state, request: data) - self.sessionManager = requestData.sessionManager - self.callback.update(state: .selectNamespaces(requestData.itemsRequests)) + let requestData = try SpruceIDMobileSdkRs.handleRequest(state: state, request: data) + sessionManager = requestData.sessionManager + callback.update(state: .selectNamespaces(requestData.itemsRequests)) } catch { - self.callback.update(state: .error(.generic("\(error)"))) - self.cancel() + callback.update(state: .error(.generic("\(error)"))) + cancel() } - case .error(let error): - self.callback.update(state: .error(BleSessionError(holderBleError: error))) - self.cancel() + case let .error(error): + callback.update(state: .error(BleSessionError(holderBleError: error))) + cancel() } } } @@ -144,9 +145,9 @@ public enum BleSessionError { init(holderBleError: MdocHolderBleError) { switch holderBleError { - case .peripheral(let string): + case let .peripheral(string): self = .peripheral(string) - case .bluetooth(let string): + case let .bluetooth(string): self = .bluetooth(string) } } diff --git a/Sources/MobileSdk/MDocBLEUtils.swift b/Sources/MobileSdk/MDocBLEUtils.swift index 028919c..3590a50 100644 --- a/Sources/MobileSdk/MDocBLEUtils.swift +++ b/Sources/MobileSdk/MDocBLEUtils.swift @@ -77,35 +77,33 @@ func MDocCharacteristicName(_ ch: CBCharacteristic) -> String { /// Return a string describing a BLE characteristic given its UUID. func MDocCharacteristicNameFromUUID(_ ch: CBUUID) -> String { return switch ch { - case holderStateCharacteristicId: "Holder:State" + case holderStateCharacteristicId: "Holder:State" case holderClient2ServerCharacteristicId: "Holder:Client2Server" case holderServer2ClientCharacteristicId: "Holder:Server2Client" - case holderL2CAPCharacteristicId: "Holder:L2CAP" - - case readerStateCharacteristicId: "Reader:State" + case holderL2CAPCharacteristicId: "Holder:L2CAP" + case readerStateCharacteristicId: "Reader:State" case readerClient2ServerCharacteristicId: "Reader:Client2Server" case readerServer2ClientCharacteristicId: "Reader:Server2Client" - case readerIdentCharacteristicId: "Reader:Ident" - case readerL2CAPCharacteristicId: "Reader:L2CAP" - - default: "Unknown:\(ch)" + case readerIdentCharacteristicId: "Reader:Ident" + case readerL2CAPCharacteristicId: "Reader:L2CAP" + default: "Unknown:\(ch)" } } /// Print a description of a BLE characteristic. func MDocDesribeCharacteristic(_ ch: CBCharacteristic) { - print(" \(MDocCharacteristicName(ch)) ( ", terminator:"") - - if(ch.properties.contains(.broadcast)) { print("broadcast", terminator:" ") } - if(ch.properties.contains(.read)) { print("read", terminator:" ") } - if(ch.properties.contains(.writeWithoutResponse)) { print("writeWithoutResponse", terminator:" ") } - if(ch.properties.contains(.write)) { print("write", terminator:" ") } - if(ch.properties.contains(.notify)) { print("notify", terminator:" ") } - if(ch.properties.contains(.indicate)) { print("indicate", terminator:" ") } - if(ch.properties.contains(.authenticatedSignedWrites)) { print("authenticatedSignedWrites", terminator:" ") } - if(ch.properties.contains(.extendedProperties)) { print("extendedProperties", terminator:" ") } - if(ch.properties.contains(.notifyEncryptionRequired)) { print("notifyEncryptionRequired", terminator:" ") } - if(ch.properties.contains(.indicateEncryptionRequired)) { print("indicateEncryptionRequired", terminator:" ") } + print(" \(MDocCharacteristicName(ch)) ( ", terminator: "") + + if ch.properties.contains(.broadcast) { print("broadcast", terminator: " ") } + if ch.properties.contains(.read) { print("read", terminator: " ") } + if ch.properties.contains(.writeWithoutResponse) { print("writeWithoutResponse", terminator: " ") } + if ch.properties.contains(.write) { print("write", terminator: " ") } + if ch.properties.contains(.notify) { print("notify", terminator: " ") } + if ch.properties.contains(.indicate) { print("indicate", terminator: " ") } + if ch.properties.contains(.authenticatedSignedWrites) { print("authenticatedSignedWrites", terminator: " ") } + if ch.properties.contains(.extendedProperties) { print("extendedProperties", terminator: " ") } + if ch.properties.contains(.notifyEncryptionRequired) { print("notifyEncryptionRequired", terminator: " ") } + if ch.properties.contains(.indicateEncryptionRequired) { print("indicateEncryptionRequired", terminator: " ") } print(")") if let descriptors = ch.descriptors { diff --git a/Sources/MobileSdk/MDocHolderBLECentral.swift b/Sources/MobileSdk/MDocHolderBLECentral.swift index 31ba0be..02f8289 100644 --- a/Sources/MobileSdk/MDocHolderBLECentral.swift +++ b/Sources/MobileSdk/MDocHolderBLECentral.swift @@ -36,7 +36,7 @@ class MDocHolderBLECentral: NSObject { var writeCharacteristic: CBCharacteristic? var readCharacteristic: CBCharacteristic? var stateCharacteristic: CBCharacteristic? - var l2capCharacteristic: CBCharacteristic? + var l2capCharacteristic: CBCharacteristic? var maximumCharacteristicSize: Int? var writingQueueTotalChunks = 0 @@ -63,7 +63,7 @@ class MDocHolderBLECentral: NSObject { self.serviceUuid = serviceUuid self.callback = callback super.init() - self.centralManager = CBCentralManager(delegate: self, queue: nil) + centralManager = CBCentralManager(delegate: self, queue: nil) } /// Update the state machine. @@ -80,19 +80,17 @@ class MDocHolderBLECentral: NSObject { update = false switch machineState { - - /// Core. + /// Core. case .initial: // Object just initialized, hardware not ready. if machinePendingState == .hardwareOn { machineState = machinePendingState update = true } - + case .hardwareOn: // Hardware is ready. centralManager.scanForPeripherals(withServices: [serviceUuid]) machineState = machinePendingState machinePendingState = .awaitPeripheralDiscovery - break case .awaitPeripheralDiscovery: if machinePendingState == .peripheralDiscovered { @@ -104,19 +102,19 @@ class MDocHolderBLECentral: NSObject { machineState = machinePendingState centralManager?.stopScan() - self.callback.callback(message: .connected) + callback.callback(message: .connected) } case .checkPeripheral: if machinePendingState == .awaitRequest { - if let peri = peripheral { if useL2CAP, let l2capC = l2capCharacteristic { peri.setNotifyValue(true, for: l2capC) peri.readValue(for: l2capC) machineState = .l2capAwaitRequest } else if let readC = readCharacteristic, - let stateC = stateCharacteristic { + let stateC = stateCharacteristic + { peri.setNotifyValue(true, for: readC) peri.setNotifyValue(true, for: stateC) peri.writeValue(_: Data([0x01]), for: stateC, type: .withoutResponse) @@ -125,7 +123,7 @@ class MDocHolderBLECentral: NSObject { } } - /// Original flow. + /// Original flow. case .awaitRequest: if machinePendingState == .requestReceived { machineState = machinePendingState @@ -133,8 +131,8 @@ class MDocHolderBLECentral: NSObject { incomingMessageBuffer = Data() } - /// The request has been received, we're waiting for the user to respond to the selective diclosure - /// dialog. + /// The request has been received, we're waiting for the user to respond to the selective diclosure + /// dialog. case .requestReceived: if machinePendingState == .sendingResponse { machineState = machinePendingState @@ -151,7 +149,7 @@ class MDocHolderBLECentral: NSObject { machineState = machinePendingState } - /// L2CAP flow. + /// L2CAP flow. case .l2capAwaitRequest: if machinePendingState == .l2capRequestReceived { machineState = machinePendingState @@ -159,8 +157,8 @@ class MDocHolderBLECentral: NSObject { incomingMessageBuffer = Data() } - /// The request has been received, we're waiting for the user to respond to the selective diclosure - /// dialog. + /// The request has been received, we're waiting for the user to respond to the selective diclosure + /// dialog. case .l2capRequestReceived: if machinePendingState == .l2capSendingResponse { machineState = machinePendingState @@ -174,12 +172,12 @@ class MDocHolderBLECentral: NSObject { machineState = machinePendingState } - // + // case .fatalError: // Something went wrong. machineState = .halted machinePendingState = .halted - + case .complete: // Transfer complete. break @@ -188,8 +186,8 @@ class MDocHolderBLECentral: NSObject { } } } - - func disconnectFromDevice () { + + func disconnectFromDevice() { let message: Data do { message = try terminateSession() @@ -204,7 +202,7 @@ class MDocHolderBLECentral: NSObject { } private func disconnect() { - if let peripheral = self.peripheral { + if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } } @@ -236,7 +234,7 @@ class MDocHolderBLECentral: NSObject { chunk.reverse() chunk.append(firstByte) chunk.reverse() - self.callback.callback(message: .uploadProgress(writingQueueChunkIndex, writingQueueTotalChunks)) + callback.callback(message: .uploadProgress(writingQueueChunkIndex, writingQueueTotalChunks)) peripheral?.writeValue(_: chunk, for: writeCharacteristic!, type: CBCharacteristicWriteType.withoutResponse) @@ -244,7 +242,7 @@ class MDocHolderBLECentral: NSObject { machinePendingState = .complete } } else { - self.callback.callback(message: .uploadProgress(writingQueueTotalChunks, writingQueueTotalChunks)) + callback.callback(message: .uploadProgress(writingQueueTotalChunks, writingQueueTotalChunks)) writingQueue = nil machinePendingState = .complete } @@ -255,7 +253,7 @@ class MDocHolderBLECentral: NSObject { private func getCharacteristic(list: [CBCharacteristic], uuid: CBUUID, properties: [CBCharacteristicProperties], required: Bool) throws -> CBCharacteristic? { let chName = MDocCharacteristicNameFromUUID(uuid) - if let candidate = list.first(where: {$0.uuid == uuid}) { + if let candidate = list.first(where: { $0.uuid == uuid }) { for prop in properties { if !candidate.properties.contains(prop) { let propName = MDocCharacteristicPropertyName(prop) @@ -278,7 +276,6 @@ class MDocHolderBLECentral: NSObject { /// Check that the reqiured characteristics are available with the required properties. func processCharacteristics(peripheral: CBPeripheral, characteristics: [CBCharacteristic]) throws { - stateCharacteristic = try getCharacteristic(list: characteristics, uuid: readerStateCharacteristicId, properties: [.notify, .writeWithoutResponse], @@ -288,7 +285,7 @@ class MDocHolderBLECentral: NSObject { uuid: readerClient2ServerCharacteristicId, properties: [.writeWithoutResponse], required: true) - + readCharacteristic = try getCharacteristic(list: characteristics, uuid: readerServer2ClientCharacteristicId, properties: [.notify], @@ -297,10 +294,11 @@ class MDocHolderBLECentral: NSObject { if let readerIdent = try getCharacteristic(list: characteristics, uuid: readerIdentCharacteristicId, properties: [.read], - required: true) { + required: true) + { peripheral.readValue(for: readerIdent) } - + l2capCharacteristic = try getCharacteristic(list: characteristics, uuid: readerL2CAPCharacteristicId, properties: [.read], @@ -308,11 +306,10 @@ class MDocHolderBLECentral: NSObject { // iOS controls MTU negotiation. Since MTU is just a maximum, we can use a lower value than the negotiated value. // 18013-5 expects an upper limit of 515 MTU, so we cap at this even if iOS negotiates a higher value. -// +// // maximumWriteValueLength() returns the maximum characteristic size, which is 3 less than the MTU. - let negotiatedMaximumCharacteristicSize = peripheral.maximumWriteValueLength(for: .withoutResponse) - maximumCharacteristicSize = min(negotiatedMaximumCharacteristicSize - 3, 512) - + let negotiatedMaximumCharacteristicSize = peripheral.maximumWriteValueLength(for: .withoutResponse) + maximumCharacteristicSize = min(negotiatedMaximumCharacteristicSize - 3, 512) } /// Process incoming data from a peripheral. This handles incoming data from any and all characteristics (though not @@ -321,21 +318,20 @@ class MDocHolderBLECentral: NSObject { if var data = characteristic.value { print("Processing \(data.count) bytes for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") switch characteristic.uuid { - - /// Transfer indicator. + /// Transfer indicator. case readerStateCharacteristicId: if data.count != 1 { throw DataError.invalidStateLength } switch data[0] { case 0x02: - self.callback.callback(message: .done) - self.disconnect() + callback.callback(message: .done) + disconnect() case let byte: throw DataError.unknownState(byte: byte) } - /// Incoming request. + /// Incoming request. case readerServer2ClientCharacteristicId: let firstByte = data.popFirst() incomingMessageBuffer.append(data) @@ -349,13 +345,13 @@ class MDocHolderBLECentral: NSObject { case 0x01: // partial print("Chunk") - // TODO check length against MTU + // TODO: check length against MTU case let .some(byte): throw DataError.unknownDataTransferPrefix(byte: byte) } - /// Ident check. + /// Ident check. case readerIdentCharacteristicId: // Looks like this should just happen after discovering characteristics print("Ident") @@ -363,7 +359,7 @@ class MDocHolderBLECentral: NSObject { // to the callback to see if the caller likes it. machinePendingState = .awaitRequest - /// L2CAP channel ID. + /// L2CAP channel ID. case readerL2CAPCharacteristicId: print("PSM: ", terminator: "") if data.count == 2 { @@ -385,21 +381,21 @@ class MDocHolderBLECentral: NSObject { } extension MDocHolderBLECentral: CBCentralManagerDelegate { - /// Handle a state change in the central manager. func centralManagerDidUpdateState(_ central: CBCentralManager) { if central.state == .poweredOn { machinePendingState = .hardwareOn } else { - self.callback.callback(message: .error(.bluetooth(central))) + callback.callback(message: .error(.bluetooth(central))) } } /// Handle discovering a peripheral. - func centralManager(_ central: CBCentralManager, + func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, - advertisementData: [String: Any], - rssi RSSI: NSNumber) { + advertisementData _: [String: Any], + rssi _: NSNumber) + { print("Discovered peripheral") peripheral.delegate = self self.peripheral = peripheral @@ -408,18 +404,17 @@ extension MDocHolderBLECentral: CBCentralManagerDelegate { } /// Handle connecting to a peripheral. - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { - peripheral.discoverServices([self.serviceUuid]) + func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { + peripheral.discoverServices([serviceUuid]) machinePendingState = .checkPeripheral } } extension MDocHolderBLECentral: CBPeripheralDelegate { - /// Handle discovery of peripheral services. func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { - if (error) != nil { - self.callback.callback( + if error != nil { + callback.callback( message: .error(.peripheral("Error discovering services: \(error!.localizedDescription)")) ) return @@ -434,8 +429,8 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { /// Handle discovery of characteristics for a peripheral service. func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { - if (error) != nil { - self.callback.callback( + if error != nil { + callback.callback( message: .error(.peripheral("Error discovering characteristics: \(error!.localizedDescription)")) ) return @@ -443,9 +438,9 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { if let characteristics = service.characteristics { print("Discovered characteristics") do { - try self.processCharacteristics(peripheral: peripheral, characteristics: characteristics) + try processCharacteristics(peripheral: peripheral, characteristics: characteristics) } catch { - self.callback.callback(message: .error(.peripheral("\(error)"))) + callback.callback(message: .error(.peripheral("\(error)"))) centralManager?.cancelPeripheralConnection(peripheral) } } @@ -454,9 +449,9 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { /// Handle a characteristic value being updated. func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { do { - try self.processData(peripheral: peripheral, characteristic: characteristic) + try processData(peripheral: peripheral, characteristic: characteristic) } catch { - self.callback.callback(message: .error(.peripheral("\(error)"))) + callback.callback(message: .error(.peripheral("\(error)"))) centralManager?.cancelPeripheralConnection(peripheral) } } @@ -465,7 +460,7 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { /// This is called after the buffer gets filled to capacity, and then has space again. /// /// Only available on iOS 11 and up. - func peripheralIsReady(toSendWriteWithoutResponse peripheral: CBPeripheral) { + func peripheralIsReady(toSendWriteWithoutResponse _: CBPeripheral) { drainWritingQueue() } @@ -482,7 +477,6 @@ extension MDocHolderBLECentral: CBPeripheralDelegate { } extension MDocHolderBLECentral: CBPeripheralManagerDelegate { - /// Handle peripheral manager state change. func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { switch peripheral.state { @@ -505,20 +499,18 @@ extension MDocHolderBLECentral: CBPeripheralManagerDelegate { } extension MDocHolderBLECentral: MDocHolderBLECentralConnectionDelegate { - func request(_ data: Data) { incomingMessageBuffer = data machinePendingState = .l2capRequestReceived } - func sendUpdate(bytes: Int, total: Int, fraction: Double) { - self.callback.callback(message: .uploadProgress(bytes, total)) + func sendUpdate(bytes: Int, total: Int, fraction _: Double) { + callback.callback(message: .uploadProgress(bytes, total)) } func sendComplete() { machinePendingState = .complete } - func connectionEnd() { - } + func connectionEnd() {} } diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index f4d5492..758c15c 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -10,8 +10,8 @@ public class MDocReader { self.callback = callback do { let sessionData = try SpruceIDMobileSdkRs.establishSession(uri: uri, requestedItems: requestedItems, trustAnchorRegistry: trustAnchorRegistry) - self.sessionManager = sessionData.state - self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, bleIdent: sessionData.bleIdent) + sessionManager = sessionData.state + bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, bleIdent: sessionData.bleIdent) } catch { print("\(error)") return nil @@ -26,24 +26,24 @@ public class MDocReader { extension MDocReader: MDocReaderBLEDelegate { func callback(message: MDocReaderBLECallback) { switch message { - case .done(let data): - self.callback.update(state: .success(data)) + case let .done(data): + 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): + callback.update(state: .connected) + case let .error(error): + callback.update(state: .error(BleReaderSessionError(readerBleError: error))) + cancel() + case let .message(data): do { - let responseData = try SpruceIDMobileSdkRs.handleResponse(state: self.sessionManager, response: data) - self.sessionManager = responseData.state - self.callback.update(state: .success(responseData.verifiedResponse)) + let responseData = try SpruceIDMobileSdkRs.handleResponse(state: sessionManager, response: data) + sessionManager = responseData.state + callback.update(state: .success(responseData.verifiedResponse)) } catch { - self.callback.update(state: .error(.generic("\(error)"))) - self.cancel() + callback.update(state: .error(.generic("\(error)"))) + cancel() } - case .downloadProgress(let index): - self.callback.update(state: .downloadProgress(index)) + case let .downloadProgress(index): + callback.update(state: .downloadProgress(index)) } } } @@ -78,9 +78,9 @@ public enum BleReaderSessionError { init(readerBleError: MdocReaderBleError) { switch readerBleError { - case .server(let string): + case let .server(string): self = .server(string) - case .bluetooth(let string): + case let .bluetooth(string): self = .bluetooth(string) } } diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index d8704a8..95ab03f 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -56,10 +56,10 @@ class MDocReaderBLEPeripheral: NSObject { self.serviceUuid = serviceUuid self.callback = callback self.bleIdent = bleIdent - self.requestData = request - self.incomingMessageBuffer = Data() + requestData = request + incomingMessageBuffer = Data() super.init() - self.peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) + peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) } /// Update the state machine. @@ -76,8 +76,7 @@ class MDocReaderBLEPeripheral: NSObject { update = false switch machineState { - - /// Core. + /// Core. case .initial: // Object just initialized, hardware not ready. if machinePendingState == .hardwareOn { machineState = .hardwareOn @@ -100,19 +99,18 @@ class MDocReaderBLEPeripheral: NSObject { machineState = machinePendingState update = true } - break case .fatalError: // Something went wrong. machineState = .halted machinePendingState = .halted - + case .complete: // Transfer complete. break case .halted: // Transfer incomplete, but we gave up. break - /// L2CAP flow. + /// L2CAP flow. case .l2capRead: // We have a read on our L2CAP characteristic, start L2CAP flow. machineState = .l2capAwaitChannelPublished peripheralManager.publishL2CAPChannel(withEncryption: true) @@ -128,28 +126,27 @@ class MDocReaderBLEPeripheral: NSObject { machineState = machinePendingState update = true } - + case .l2capStreamOpen: // An L2CAP stream is opened. activeStream?.send(data: requestData) machineState = .l2capSendingRequest machinePendingState = .l2capSendingRequest update = true - + case .l2capSendingRequest: // The request is being sent over the L2CAP stream. if machinePendingState == .l2capAwaitingResponse { machineState = machinePendingState update = true } - break - + case .l2capAwaitingResponse: // The request is sent, the response is (hopefully) coming in. if machinePendingState == .complete { machineState = machinePendingState callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) update = true } - - /// Original flow. + + /// Original flow. case .stateSubscribed: // We have a subscription to our State characteristic, start original flow. // This will trigger wallet-sdk-swift to send 0x01 to start the exchange peripheralManager.updateValue(bleIdent, for: identCharacteristic!, onSubscribedCentrals: nil) @@ -157,13 +154,13 @@ class MDocReaderBLEPeripheral: NSObject { // I think the updateValue() below is out of spec; 8.3.3.1.1.5 says we wait for a write without // response of 0x01 to State, but that's supposed to come from the holder to indicate it's ready // for us to initiate. - + // This will trigger wallet-sdk-kt to send 0x01 to start the exchange - //peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + // peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) machineState = .awaitRequestStart machinePendingState = .awaitRequestStart - + case .awaitRequestStart: // We've let the holder know we're ready, waiting for their ack. if machinePendingState == .sendingRequest { writeOutgoingValue(data: requestData) @@ -174,76 +171,73 @@ class MDocReaderBLEPeripheral: NSObject { if machinePendingState == .awaitResponse { machineState = .awaitResponse } - break case .awaitResponse: if machinePendingState == .complete { machineState = .complete update = true } - break } } } func setupService() { - let service = CBMutableService(type: self.serviceUuid, primary: true) + let service = CBMutableService(type: 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]) + 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]) + readCharacteristic = CBMutableCharacteristic(type: readerClient2ServerCharacteristicId, + properties: [.writeWithoutResponse, .write], + value: nil, + permissions: [.writeable]) + 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]) + identCharacteristic = CBMutableCharacteristic(type: readerIdentCharacteristicId, + properties: [.read], + value: bleIdent, + permissions: [.readable]) // wallet-sdk-kt is failing if this is present if useL2CAP { // 18013-5 doesn't require .indicate, but without it we don't seem to be able to propagate the PSM // through to central. - self.l2capCharacteristic = CBMutableCharacteristic(type: readerL2CAPCharacteristicId, - properties: [.read, .indicate], - value: nil, - permissions: [.readable]) + l2capCharacteristic = CBMutableCharacteristic(type: readerL2CAPCharacteristicId, + properties: [.read, .indicate], + value: nil, + permissions: [.readable]) if let stateC = stateCharacteristic, let readC = readCharacteristic, let writeC = writeCharacteristic, let identC = identCharacteristic, - let l2capC = l2capCharacteristic { - + let l2capC = l2capCharacteristic + { service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC, l2capC] } } else { if let stateC = stateCharacteristic, let readC = readCharacteristic, let writeC = writeCharacteristic, - let identC = identCharacteristic { + let identC = identCharacteristic + { service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC] } } peripheralManager.add(service) } - func disconnect() { - return - } + func disconnect() {} /// Write the request using the old flow. func writeOutgoingValue(data: Data) { @@ -267,7 +261,7 @@ class MDocReaderBLEPeripheral: NSObject { chunk.reverse() chunk.append(firstByte) chunk.reverse() - self.peripheralManager?.updateValue(chunk, for: self.writeCharacteristic!, onSubscribedCentrals: nil) + peripheralManager?.updateValue(chunk, for: writeCharacteristic!, onSubscribedCentrals: nil) if firstByte == 0x00 { machinePendingState = .awaitResponse @@ -280,11 +274,10 @@ class MDocReaderBLEPeripheral: NSObject { } /// Process incoming data. - func processData(central: CBCentral, characteristic: CBCharacteristic, value: Data?) throws { + func processData(central _: CBCentral, characteristic: CBCharacteristic, value: Data?) throws { if var data = value { print("Processing \(data.count) bytes of data for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") switch characteristic.uuid { - case readerClient2ServerCharacteristicId: let firstByte = data.popFirst() incomingMessageBuffer.append(data) @@ -294,24 +287,24 @@ class MDocReaderBLEPeripheral: NSObject { throw DataError.noData(characteristic: characteristic.uuid) case 0x00: // end print("End") - self.callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) - self.incomingMessageBuffer = Data() - self.incomingMessageIndex = 0 + callback.callback(message: MDocReaderBLECallback.message(incomingMessageBuffer)) + incomingMessageBuffer = Data() + incomingMessageIndex = 0 machinePendingState = .complete return case 0x01: // partial print("Chunk") - self.incomingMessageIndex += 1 - self.callback.callback(message: .downloadProgress(self.incomingMessageIndex)) - // TODO check length against MTU + incomingMessageIndex += 1 + callback.callback(message: .downloadProgress(incomingMessageIndex)) + // TODO: check length against MTU return case let .some(byte): print("Unexpected byte \(String(format: "$%02X", byte))") throw DataError.unknownDataTransferPrefix(byte: byte) } - case readerStateCharacteristicId: - print("State") + case readerStateCharacteristicId: + print("State") if data.count != 1 { throw DataError.invalidStateLength } @@ -322,13 +315,13 @@ class MDocReaderBLEPeripheral: NSObject { throw DataError.unknownState(byte: byte) } - case readerL2CAPCharacteristicId: - print("L2CAP") - machinePendingState = .l2capRead - return + case readerL2CAPCharacteristicId: + print("L2CAP") + machinePendingState = .l2capRead + return - case let uuid: - print("Unexpected UUID") + case let uuid: + print("Unexpected UUID") throw DataError.unknownCharacteristic(uuid: uuid) } } else { @@ -352,7 +345,6 @@ class MDocReaderBLEPeripheral: NSObject { /// Peripheral manager delegate functions. extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { - /// Handle the peripheral updating state. func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { switch peripheral.state { @@ -374,15 +366,15 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } /// Handle space available for sending. This is part of the send loop for the old (non-L2CAP) flow. - func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) { - self.drainWritingQueue() + func peripheralManagerIsReady(toUpdateSubscribers _: CBPeripheralManager) { + drainWritingQueue() } /// Handle incoming subscriptions. - func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { + func peripheralManager(_: CBPeripheralManager, central _: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { print("Subscribed to \(MDocCharacteristicNameFromUUID(characteristic.uuid))") - self.callback.callback(message: .connected) - self.peripheralManager?.stopAdvertising() + callback.callback(message: .connected) + peripheralManager?.stopAdvertising() switch characteristic.uuid { case l2capCharacteristic: // If we get this, we're in the L2CAP flow. // TODO: If this gets hit after a subscription to the State characteristic, something has gone wrong; @@ -393,7 +385,6 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { // - try to adapt -- send the data a second time, listen on both L2CAP and normal - probably a bad idea; // it will make us mildly more tolerant of out-of-spec holders, but may increase our attack surface machinePendingState = .l2capRead - break case readerStateCharacteristicId: // If we get this, we're in the original flow. // TODO: See the comment block in the L2CAP characteristic, above; only one of these can be valid for @@ -407,28 +398,28 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } /// Handle read requests. - func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) { + func peripheralManager(_: CBPeripheralManager, didReceiveRead request: CBATTRequest) { print("Received read request for \(MDocCharacteristicNameFromUUID(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) { + + if request.characteristic.uuid == readerIdentCharacteristicId { peripheralManager.respond(to: request, withResult: .success) - } else if (request.characteristic.uuid == readerL2CAPCharacteristicId) { + } else if request.characteristic.uuid == readerL2CAPCharacteristicId { peripheralManager.respond(to: request, withResult: .success) machinePendingState = .l2capRead } else { - self.callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(MDocCharacteristicNameFromUUID(request.characteristic.uuid))"))) + callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(MDocCharacteristicNameFromUUID(request.characteristic.uuid))"))) } } /// Handle write requests. - func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) { + func peripheralManager(_: 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 { 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 @@ -436,8 +427,8 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { peripheralManager.respond(to: request, withResult: .success) } } catch { - self.callback.callback(message: .error(.server("\(error)"))) - self.peripheralManager?.updateValue(Data([0x02]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + callback.callback(message: .error(.server("\(error)"))) + peripheralManager?.updateValue(Data([0x02]), for: stateCharacteristic!, onSubscribedCentrals: nil) } } } diff --git a/Sources/MobileSdk/PlatformContext.swift b/Sources/MobileSdk/PlatformContext.swift index f4d1039..bcafb31 100644 --- a/Sources/MobileSdk/PlatformContext.swift +++ b/Sources/MobileSdk/PlatformContext.swift @@ -18,8 +18,8 @@ import Foundation // A container for platform-specific subsystems. class SpruceKitPlatformContext: NSObject { - let keyMgr = KeyManager() // Keys - let storageMgr = StorageManager() // Secure storage. + let keyMgr = KeyManager() // Keys + let storageMgr = StorageManager() // Secure storage. } // diff --git a/Sources/MobileSdk/StorageManager.swift b/Sources/MobileSdk/StorageManager.swift index daeb404..bf128bf 100644 --- a/Sources/MobileSdk/StorageManager.swift +++ b/Sources/MobileSdk/StorageManager.swift @@ -31,9 +31,9 @@ class StorageManager: NSObject, StorageManagerInterface { let bundle = Bundle.main let asdir = try fileman.url(for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, // Ignored - create: true) // May not exist, make if necessary. + in: .userDomainMask, + appropriateFor: nil, // Ignored + create: true) // May not exist, make if necessary. // If we create subdirectories in the application support directory, we need to put them in a subdir // named after the app; normally, that's `CFBundleDisplayName` from `info.plist`, but that key doesn't diff --git a/Sources/MobileSdk/W3CVC.swift b/Sources/MobileSdk/W3CVC.swift index f9516a1..5596244 100644 --- a/Sources/MobileSdk/W3CVC.swift +++ b/Sources/MobileSdk/W3CVC.swift @@ -13,13 +13,13 @@ public class W3CVC: Credential { if let data = credentialString.data(using: .utf8) { do { let json = try JSONDecoder().decode(GenericJSON.self, from: data) - self.credential = json + credential = json super.init(id: json["id"]!.toString()) } catch let error as NSError { throw error } } else { - self.credential = nil + credential = nil super.init(id: "") throw W3CError.initializationError("Failed to process credential string.") } @@ -31,6 +31,5 @@ public class W3CVC: Credential { } else { return [:] } - } } From dee7aaad6eb65b70856c98538b76bf23e10b5f2e Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Thu, 5 Sep 2024 01:51:01 -0400 Subject: [PATCH 12/17] Some fixes for the linter. --- Sources/MobileSdk/MDocHolderBLECentralConnection.swift | 2 +- Sources/MobileSdk/MDocReaderBLEPeripheral.swift | 6 ++---- Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/MobileSdk/MDocHolderBLECentralConnection.swift b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift index 4663b74..fc6b81a 100644 --- a/Sources/MobileSdk/MDocHolderBLECentralConnection.swift +++ b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift @@ -3,7 +3,7 @@ import CoreBluetooth import Foundation -public protocol MDocHolderBLECentralConnectionDelegate { +public protocol MDocHolderBLECentralConnectionDelegate: AnyObject { func request(_ data: Data) func sendUpdate(bytes: Int, total: Int, fraction: Double) func sendComplete() diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index 95ab03f..60ee457 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -221,16 +221,14 @@ class MDocReaderBLEPeripheral: NSObject { let readC = readCharacteristic, let writeC = writeCharacteristic, let identC = identCharacteristic, - let l2capC = l2capCharacteristic - { + let l2capC = l2capCharacteristic { service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC, l2capC] } } else { if let stateC = stateCharacteristic, let readC = readCharacteristic, let writeC = writeCharacteristic, - let identC = identCharacteristic - { + let identC = identCharacteristic { service.characteristics = (service.characteristics ?? []) + [stateC, readC, writeC, identC] } } diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift index 69d1fbd..86e7551 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift @@ -3,7 +3,7 @@ import CoreBluetooth import Foundation -protocol MDocReaderBLEPeripheralConnectionDelegate { +protocol MDocReaderBLEPeripheralConnectionDelegate: AnyObject { func streamOpen() func sentData(_ bytes: Int) func receivedData(_ data: Data) From 8a60babbb243fcb3561afb3f95ebc498c7b7b648 Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Thu, 5 Sep 2024 10:56:49 -0400 Subject: [PATCH 13/17] Linter fixes. --- .swiftlint.yml | 2 + Sources/MobileSdk/BLEConnection.swift | 18 ++++---- Sources/MobileSdk/KeyManager.swift | 16 +++---- Sources/MobileSdk/MDocBLEUtils.swift | 40 ++++++++--------- Sources/MobileSdk/MDocHolderBLECentral.swift | 32 +++++++------- .../MobileSdk/MDocReaderBLEPeripheral.swift | 43 +++++++++++-------- .../MDocReaderBLEPeripheralConnection.swift | 6 +-- 7 files changed, 82 insertions(+), 75 deletions(-) diff --git a/.swiftlint.yml b/.swiftlint.yml index 57a0484..4fba1ac 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -4,5 +4,7 @@ disabled_rules: - cyclomatic_complexity - todo - file_length + - function_body_length + - type_body_length - force_try - non_optional_string_data_conversion diff --git a/Sources/MobileSdk/BLEConnection.swift b/Sources/MobileSdk/BLEConnection.swift index 2575c17..b86975b 100644 --- a/Sources/MobileSdk/BLEConnection.swift +++ b/Sources/MobileSdk/BLEConnection.swift @@ -34,7 +34,9 @@ class BLEInternalConnection: NSObject, StreamDelegate { case Stream.Event.hasBytesAvailable: streamBytesAvailable() - readBytes(from: aStream as! InputStream) + if let stream = aStream as? InputStream { + readBytes(from: stream) + } case Stream.Event.hasSpaceAvailable: streamSpaceAvailable() @@ -78,13 +80,13 @@ class BLEInternalConnection: NSObject, StreamDelegate { /// Close the stream. public func close() { - if let ch = channel { - ch.outputStream.close() - ch.inputStream.close() - ch.inputStream.remove(from: .main, forMode: .default) - ch.outputStream.remove(from: .main, forMode: .default) - ch.inputStream.delegate = nil - ch.outputStream.delegate = nil + if let chn = channel { + chn.outputStream.close() + chn.inputStream.close() + chn.inputStream.remove(from: .main, forMode: .default) + chn.outputStream.remove(from: .main, forMode: .default) + chn.inputStream.delegate = nil + chn.outputStream.delegate = nil openCount = 0 } diff --git a/Sources/MobileSdk/KeyManager.swift b/Sources/MobileSdk/KeyManager.swift index 7c276ce..527ed1a 100644 --- a/Sources/MobileSdk/KeyManager.swift +++ b/Sources/MobileSdk/KeyManager.swift @@ -8,7 +8,7 @@ public class KeyManager: NSObject { */ public static func reset() -> Bool { let query: [String: Any] = [ - kSecClass as String: kSecClassKey, + kSecClass as String: kSecClassKey ] let ret = SecItemDelete(query as CFDictionary) @@ -24,7 +24,7 @@ public class KeyManager: NSObject { kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: tag, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, + kSecReturnRef as String: true ] var item: CFTypeRef? @@ -41,7 +41,7 @@ public class KeyManager: NSObject { kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: tag, kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true, + kSecReturnRef as String: true ] var item: CFTypeRef? @@ -77,8 +77,8 @@ public class KeyManager: NSObject { kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true, kSecAttrApplicationTag as String: tag, - kSecAttrAccessControl as String: access, - ], + kSecAttrAccessControl as String: access + ] ] var error: Unmanaged? @@ -113,7 +113,7 @@ public class KeyManager: NSObject { "kty": "EC", "crv": "P-256", "x": xCoordinate, - "y": yCoordinate, + "y": yCoordinate ] guard let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: []) else { return nil } @@ -168,8 +168,8 @@ public class KeyManager: NSObject { kSecPrivateKeyAttrs as String: [ kSecAttrIsPermanent as String: true, kSecAttrApplicationTag as String: tag, - kSecAttrAccessControl as String: access, - ], + kSecAttrAccessControl as String: access + ] ] var error: Unmanaged? diff --git a/Sources/MobileSdk/MDocBLEUtils.swift b/Sources/MobileSdk/MDocBLEUtils.swift index 3590a50..c9d411a 100644 --- a/Sources/MobileSdk/MDocBLEUtils.swift +++ b/Sources/MobileSdk/MDocBLEUtils.swift @@ -70,13 +70,13 @@ func MDocCharacteristicPropertyName(_ prop: CBCharacteristicProperties) -> Strin } /// Return a string describing a BLE characteristic. -func MDocCharacteristicName(_ ch: CBCharacteristic) -> String { - return MDocCharacteristicNameFromUUID(ch.uuid) +func MDocCharacteristicName(_ chr: CBCharacteristic) -> String { + return MDocCharacteristicNameFromUUID(chr.uuid) } /// Return a string describing a BLE characteristic given its UUID. -func MDocCharacteristicNameFromUUID(_ ch: CBUUID) -> String { - return switch ch { +func MDocCharacteristicNameFromUUID(_ chr: CBUUID) -> String { + return switch chr { case holderStateCharacteristicId: "Holder:State" case holderClient2ServerCharacteristicId: "Holder:Client2Server" case holderServer2ClientCharacteristicId: "Holder:Server2Client" @@ -86,29 +86,29 @@ func MDocCharacteristicNameFromUUID(_ ch: CBUUID) -> String { case readerServer2ClientCharacteristicId: "Reader:Server2Client" case readerIdentCharacteristicId: "Reader:Ident" case readerL2CAPCharacteristicId: "Reader:L2CAP" - default: "Unknown:\(ch)" + default: "Unknown:\(chr)" } } /// Print a description of a BLE characteristic. -func MDocDesribeCharacteristic(_ ch: CBCharacteristic) { - print(" \(MDocCharacteristicName(ch)) ( ", terminator: "") +func MDocDesribeCharacteristic(_ chr: CBCharacteristic) { + print(" \(MDocCharacteristicName(chr)) ( ", terminator: "") - if ch.properties.contains(.broadcast) { print("broadcast", terminator: " ") } - if ch.properties.contains(.read) { print("read", terminator: " ") } - if ch.properties.contains(.writeWithoutResponse) { print("writeWithoutResponse", terminator: " ") } - if ch.properties.contains(.write) { print("write", terminator: " ") } - if ch.properties.contains(.notify) { print("notify", terminator: " ") } - if ch.properties.contains(.indicate) { print("indicate", terminator: " ") } - if ch.properties.contains(.authenticatedSignedWrites) { print("authenticatedSignedWrites", terminator: " ") } - if ch.properties.contains(.extendedProperties) { print("extendedProperties", terminator: " ") } - if ch.properties.contains(.notifyEncryptionRequired) { print("notifyEncryptionRequired", terminator: " ") } - if ch.properties.contains(.indicateEncryptionRequired) { print("indicateEncryptionRequired", terminator: " ") } + if chr.properties.contains(.broadcast) { print("broadcast", terminator: " ") } + if chr.properties.contains(.read) { print("read", terminator: " ") } + if chr.properties.contains(.writeWithoutResponse) { print("writeWithoutResponse", terminator: " ") } + if chr.properties.contains(.write) { print("write", terminator: " ") } + if chr.properties.contains(.notify) { print("notify", terminator: " ") } + if chr.properties.contains(.indicate) { print("indicate", terminator: " ") } + if chr.properties.contains(.authenticatedSignedWrites) { print("authenticatedSignedWrites", terminator: " ") } + if chr.properties.contains(.extendedProperties) { print("extendedProperties", terminator: " ") } + if chr.properties.contains(.notifyEncryptionRequired) { print("notifyEncryptionRequired", terminator: " ") } + if chr.properties.contains(.indicateEncryptionRequired) { print("indicateEncryptionRequired", terminator: " ") } print(")") - if let descriptors = ch.descriptors { - for d in descriptors { - print(" : \(d.uuid)") + if let descriptors = chr.descriptors { + for desc in descriptors { + print(" : \(desc.uuid)") } } else { print(" ") diff --git a/Sources/MobileSdk/MDocHolderBLECentral.swift b/Sources/MobileSdk/MDocHolderBLECentral.swift index 02f8289..9dd17e6 100644 --- a/Sources/MobileSdk/MDocHolderBLECentral.swift +++ b/Sources/MobileSdk/MDocHolderBLECentral.swift @@ -46,7 +46,7 @@ class MDocHolderBLECentral: NSObject { var incomingMessageBuffer = Data() var outgoingMessageBuffer = Data() - private var channelPSM: UInt16? = nil + private var channelPSM: UInt16? private var activeStream: MDocHolderBLECentralConnection? /// If this is `false`, we decline to connect to L2CAP even if it is offered. @@ -113,8 +113,7 @@ class MDocHolderBLECentral: NSObject { peri.readValue(for: l2capC) machineState = .l2capAwaitRequest } else if let readC = readCharacteristic, - let stateC = stateCharacteristic - { + let stateC = stateCharacteristic { peri.setNotifyValue(true, for: readC) peri.setNotifyValue(true, for: stateC) peri.writeValue(_: Data([0x01]), for: stateC, type: .withoutResponse) @@ -250,18 +249,18 @@ class MDocHolderBLECentral: NSObject { } /// Verify that a characteristic matches what is required of it. - private func getCharacteristic(list: [CBCharacteristic], uuid: CBUUID, properties: [CBCharacteristicProperties], required: Bool) throws -> CBCharacteristic? { + private func getCharacteristic(list: [CBCharacteristic], + uuid: CBUUID, properties: [CBCharacteristicProperties], + required: Bool) throws -> CBCharacteristic? { let chName = MDocCharacteristicNameFromUUID(uuid) if let candidate = list.first(where: { $0.uuid == uuid }) { - for prop in properties { - if !candidate.properties.contains(prop) { - let propName = MDocCharacteristicPropertyName(prop) - if required { - throw CharacteristicsError.missingMandatoryProperty(name: propName, characteristicName: chName) - } else { - return nil - } + for prop in properties where !candidate.properties.contains(prop) { + let propName = MDocCharacteristicPropertyName(prop) + if required { + throw CharacteristicsError.missingMandatoryProperty(name: propName, characteristicName: chName) + } else { + return nil } } return candidate @@ -294,8 +293,7 @@ class MDocHolderBLECentral: NSObject { if let readerIdent = try getCharacteristic(list: characteristics, uuid: readerIdentCharacteristicId, properties: [.read], - required: true) - { + required: true) { peripheral.readValue(for: readerIdent) } @@ -316,7 +314,8 @@ class MDocHolderBLECentral: NSObject { /// the L2CAP stream...), so we hit this call multiple times from several angles, at least in the original flow. func processData(peripheral: CBPeripheral, characteristic: CBCharacteristic) throws { if var data = characteristic.value { - print("Processing \(data.count) bytes for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") + print("Processing \(data.count) bytes for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", + terminator: "") switch characteristic.uuid { /// Transfer indicator. case readerStateCharacteristicId: @@ -394,8 +393,7 @@ extension MDocHolderBLECentral: CBCentralManagerDelegate { func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData _: [String: Any], - rssi _: NSNumber) - { + rssi _: NSNumber) { print("Discovered peripheral") peripheral.delegate = self self.peripheral = peripheral diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index 60ee457..aa77725 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -39,7 +39,7 @@ class MDocReaderBLEPeripheral: NSObject { /// of these things, and use the old flow. var useL2CAP = true - private var channelPSM: UInt16? = nil { + private var channelPSM: UInt16? { didSet { updatePSM() } @@ -59,7 +59,9 @@ class MDocReaderBLEPeripheral: NSObject { requestData = request incomingMessageBuffer = Data() super.init() - peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) + peripheralManager = CBPeripheralManager(delegate: self, + queue: nil, + options: [CBPeripheralManagerOptionShowPowerAlertKey: true]) } /// Update the state machine. @@ -67,12 +69,9 @@ class MDocReaderBLEPeripheral: NSObject { var update = true while update { - if machineState != machinePendingState { - print("「\(machineState) → \(machinePendingState)」") - } else { - print("「\(machineState)」") - } - + print(machineState == machinePendingState + ? "「\(machineState)」" + : "「\(machineState) → \(machinePendingState)」") update = false switch machineState { @@ -156,7 +155,8 @@ class MDocReaderBLEPeripheral: NSObject { // for us to initiate. // This will trigger wallet-sdk-kt to send 0x01 to start the exchange - // peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, onSubscribedCentrals: nil) + // peripheralManager.updateValue(Data([0x01]), for: self.stateCharacteristic!, + // onSubscribedCentrals: nil) machineState = .awaitRequestStart machinePendingState = .awaitRequestStart @@ -183,16 +183,17 @@ class MDocReaderBLEPeripheral: NSObject { func setupService() { let service = CBMutableService(type: serviceUuid, primary: true) - // CBUUIDClientCharacteristicConfigurationString only returns "2902" - // let clientDescriptor = CBMutableDescriptor(type: CBUUID(string: "00002902-0000-1000-8000-00805f9b34fb"), value: Data([0x00, 0x00])) as CBDescriptor + // 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... 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) + // self.stateCharacteristic!.descriptors = [clientDescriptor] + (self.stateCharacteristic!.descriptors ?? [] ) + // self.stateCharacteristic!.descriptors?.insert(clientDescriptor, at: 0) // wallet-sdk-kt isn't using write without response... readCharacteristic = CBMutableCharacteristic(type: readerClient2ServerCharacteristicId, properties: [.writeWithoutResponse, .write], @@ -202,8 +203,8 @@ class MDocReaderBLEPeripheral: NSObject { properties: [.notify], value: nil, permissions: [.readable, .writeable]) - // self.writeCharacteristic!.descriptors = [clientDescriptor] + (self.writeCharacteristic!.descriptors ?? [] ) - // self.writeCharacteristic!.descriptors?.insert(clientDescriptor, at: 0) + // self.writeCharacteristic!.descriptors = [clientDescriptor] + (self.writeCharacteristic!.descriptors ?? [] ) + // self.writeCharacteristic!.descriptors?.insert(clientDescriptor, at: 0) identCharacteristic = CBMutableCharacteristic(type: readerIdentCharacteristicId, properties: [.read], value: bleIdent, @@ -274,7 +275,8 @@ class MDocReaderBLEPeripheral: NSObject { /// Process incoming data. func processData(central _: CBCentral, characteristic: CBCharacteristic, value: Data?) throws { if var data = value { - print("Processing \(data.count) bytes of data for \(MDocCharacteristicNameFromUUID(characteristic.uuid)) → ", terminator: "") + let name = MDocCharacteristicNameFromUUID(characteristic.uuid) + print("Processing \(data.count) bytes of data for \(name) → ", terminator: "") switch characteristic.uuid { case readerClient2ServerCharacteristicId: let firstByte = data.popFirst() @@ -369,7 +371,9 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } /// Handle incoming subscriptions. - func peripheralManager(_: CBPeripheralManager, central _: CBCentral, didSubscribeTo characteristic: CBCharacteristic) { + func peripheralManager(_: CBPeripheralManager, + central _: CBCentral, + didSubscribeTo characteristic: CBCharacteristic) { print("Subscribed to \(MDocCharacteristicNameFromUUID(characteristic.uuid))") callback.callback(message: .connected) peripheralManager?.stopAdvertising() @@ -408,7 +412,8 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { peripheralManager.respond(to: request, withResult: .success) machinePendingState = .l2capRead } else { - callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(MDocCharacteristicNameFromUUID(request.characteristic.uuid))"))) + let name = MDocCharacteristicNameFromUUID(request.characteristic.uuid) + callback.callback(message: .error(.server("Read on unexpected characteristic with UUID \(name)"))) } } @@ -456,7 +461,7 @@ extension MDocReaderBLEPeripheral: CBPeripheralManagerDelegate { } /// L2CAP Stream delegate functions. -extension MDocReaderBLEPeripheral: MDocReaderBLEPeripheralConnectionDelegate { +extension MDocReaderBLEPeripheral: MDocReaderBLEPeriConnDelegate { func streamOpen() { machinePendingState = .l2capStreamOpen } diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift index 86e7551..e9e5bc4 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift @@ -3,17 +3,17 @@ import CoreBluetooth import Foundation -protocol MDocReaderBLEPeripheralConnectionDelegate: AnyObject { +protocol MDocReaderBLEPeriConnDelegate: AnyObject { func streamOpen() func sentData(_ bytes: Int) func receivedData(_ data: Data) } class MDocReaderBLEPeripheralConnection: BLEInternalConnection { - private let controlDelegate: MDocReaderBLEPeripheralConnectionDelegate + private let controlDelegate: MDocReaderBLEPeriConnDelegate /// Initialize a reader peripheral connection. - init(delegate: MDocReaderBLEPeripheralConnectionDelegate, channel: CBL2CAPChannel) { + init(delegate: MDocReaderBLEPeriConnDelegate, channel: CBL2CAPChannel) { controlDelegate = delegate super.init() self.channel = channel From 464f5e8a43841ba25a39e3bdc81a845c3ce01d71 Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Thu, 5 Sep 2024 13:47:57 -0400 Subject: [PATCH 14/17] Remove artifacts from base branch. --- Sources/MobileSdk/Credential.swift | 2 +- Sources/MobileSdk/CredentialPack.swift | 23 +++---- Sources/MobileSdk/Credentials.swift | 4 +- Sources/MobileSdk/DataConversions.swift | 16 ++--- Sources/MobileSdk/GenericJSON.swift | 25 ++++---- Sources/MobileSdk/KeyManager.swift | 82 ++++++++++++------------- Sources/MobileSdk/MDoc.swift | 73 +++++++++++----------- Sources/MobileSdk/PlatformContext.swift | 4 +- Sources/MobileSdk/StorageManager.swift | 6 +- Sources/MobileSdk/W3CVC.swift | 5 +- 10 files changed, 119 insertions(+), 121 deletions(-) diff --git a/Sources/MobileSdk/Credential.swift b/Sources/MobileSdk/Credential.swift index 75f6799..54980e9 100644 --- a/Sources/MobileSdk/Credential.swift +++ b/Sources/MobileSdk/Credential.swift @@ -9,7 +9,7 @@ open class Credential: Identifiable { open func get(keys: [String]) -> [String: GenericJSON] { if keys.contains("id") { - return ["id": GenericJSON.string(id)] + return ["id": GenericJSON.string(self.id)] } else { return [:] } diff --git a/Sources/MobileSdk/CredentialPack.swift b/Sources/MobileSdk/CredentialPack.swift index 9a44133..6bc6449 100644 --- a/Sources/MobileSdk/CredentialPack.swift +++ b/Sources/MobileSdk/CredentialPack.swift @@ -1,11 +1,12 @@ -import CryptoKit import Foundation +import CryptoKit public class CredentialPack { + private var credentials: [Credential] public init() { - credentials = [] + self.credentials = [] } public init(credentials: [Credential]) { @@ -15,8 +16,8 @@ public class CredentialPack { public func addW3CVC(credentialString: String) throws -> [Credential]? { do { let credential = try W3CVC(credentialString: credentialString) - credentials.append(credential) - return credentials + self.credentials.append(credential) + return self.credentials } catch { throw error } @@ -25,13 +26,13 @@ public class CredentialPack { public func addMDoc(mdocBase64: String, keyAlias: String = UUID().uuidString) throws -> [Credential]? { let mdocData = Data(base64Encoded: mdocBase64)! let credential = MDoc(fromMDoc: mdocData, namespaces: [:], keyAlias: keyAlias)! - credentials.append(credential) - return credentials + self.credentials.append(credential) + return self.credentials } public func get(keys: [String]) -> [String: [String: GenericJSON]] { var values: [String: [String: GenericJSON]] = [:] - for cred in credentials { + for cred in self.credentials { values[cred.id] = cred.get(keys: keys) } @@ -39,14 +40,14 @@ public class CredentialPack { } public func get(credentialsIds: [String]) -> [Credential] { - return credentials.filter { credentialsIds.contains($0.id) } + return self.credentials.filter { credentialsIds.contains($0.id) } } public func get(credentialId: String) -> Credential? { - if let credential = credentials.first(where: { $0.id == credentialId }) { - return credential + if let credential = self.credentials.first(where: { $0.id == credentialId }) { + return credential } else { - return nil + return nil } } } diff --git a/Sources/MobileSdk/Credentials.swift b/Sources/MobileSdk/Credentials.swift index 99e37be..3fd16dd 100644 --- a/Sources/MobileSdk/Credentials.swift +++ b/Sources/MobileSdk/Credentials.swift @@ -8,11 +8,11 @@ public class CredentialStore { } // swiftlint:disable force_cast - public func presentMdocBLE(deviceEngagement _: DeviceEngagement, + public func presentMdocBLE(deviceEngagement: DeviceEngagement, callback: BLESessionStateDelegate // , trustedReaders: TrustedReaders ) -> BLESessionManager? { - if let firstMdoc = credentials.first(where: { $0 is MDoc }) { + if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) { return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback) } else { return nil diff --git a/Sources/MobileSdk/DataConversions.swift b/Sources/MobileSdk/DataConversions.swift index 0d7cefc..73d3e9a 100644 --- a/Sources/MobileSdk/DataConversions.swift +++ b/Sources/MobileSdk/DataConversions.swift @@ -1,13 +1,13 @@ import Foundation extension Data { - var base64EncodedUrlSafe: String { - let string = base64EncodedString() + var base64EncodedUrlSafe: String { + let string = self.base64EncodedString() - // Make this URL safe and remove padding - return string - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } + // Make this URL safe and remove padding + return string + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } } diff --git a/Sources/MobileSdk/GenericJSON.swift b/Sources/MobileSdk/GenericJSON.swift index 6e40c2d..fb1d859 100644 --- a/Sources/MobileSdk/GenericJSON.swift +++ b/Sources/MobileSdk/GenericJSON.swift @@ -31,11 +31,11 @@ extension GenericJSON: Codable { public func toString() -> String { switch self { - case let .string(str): + case .string(let str): return str - case let .number(num): + case .number(let num): return num.debugDescription - case let .bool(bool): + case .bool(let bool): return bool.description case .null: return "null" @@ -71,11 +71,11 @@ extension GenericJSON: Codable { extension GenericJSON: CustomDebugStringConvertible { public var debugDescription: String { switch self { - case let .string(str): + case .string(let str): return str.debugDescription - case let .number(num): + case .number(let num): return num.debugDescription - case let .bool(bool): + case .bool(let bool): return bool.description case .null: return "null" @@ -89,28 +89,26 @@ extension GenericJSON: CustomDebugStringConvertible { public extension GenericJSON { var dictValue: [String: GenericJSON]? { - if case let .object(value) = self { + if case .object(let value) = self { return value } return nil } - var arrayValue: [GenericJSON]? { - if case let .array(value) = self { + if case .array(let value) = self { return value } return nil } - subscript(index: Int) -> GenericJSON? { - if case let .array(arr) = self, arr.indices.contains(index) { + if case .array(let arr) = self, arr.indices.contains(index) { return arr[index] } return nil } subscript(key: String) -> GenericJSON? { - if case let .object(dict) = self { + if case .object(let dict) = self { return dict[key] } return nil @@ -125,7 +123,7 @@ public extension GenericJSON { } func queryKeyPath(_ path: T) -> GenericJSON? where T: Collection, T.Element == String { - guard case let .object(object) = self else { + guard case .object(let object) = self else { return nil } guard let head = path.first else { @@ -137,4 +135,5 @@ public extension GenericJSON { let tail = path.dropFirst() return tail.isEmpty ? value : value.queryKeyPath(tail) } + } diff --git a/Sources/MobileSdk/KeyManager.swift b/Sources/MobileSdk/KeyManager.swift index 527ed1a..a62a687 100644 --- a/Sources/MobileSdk/KeyManager.swift +++ b/Sources/MobileSdk/KeyManager.swift @@ -36,24 +36,24 @@ public class KeyManager: NSObject { * Returns a secret key - based on the id of the key. */ public static func getSecretKey(id: String) -> SecKey? { - let tag = id.data(using: .utf8)! - let query: [String: Any] = [ - kSecClass as String: kSecClassKey, - kSecAttrApplicationTag as String: tag, - kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, - kSecReturnRef as String: true - ] + let tag = id.data(using: .utf8)! + let query: [String: Any] = [ + kSecClass as String: kSecClassKey, + kSecAttrApplicationTag as String: tag, + kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, + kSecReturnRef as String: true + ] - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + var item: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &item) - guard status == errSecSuccess else { return nil } + guard status == errSecSuccess else { return nil } - // swiftlint:disable force_cast - let key = item as! SecKey - // swiftlint:enable force_cast + // swiftlint:disable force_cast + let key = item as! SecKey + // swiftlint:enable force_cast - return key + return key } /** @@ -66,8 +66,7 @@ public class KeyManager: NSObject { kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .privateKeyUsage, - nil - )! + nil)! let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom, @@ -83,7 +82,7 @@ public class KeyManager: NSObject { var error: Unmanaged? SecKeyCreateRandomKey(attributes as CFDictionary, &error) - if error != nil { print(error!) } + if error != nil { print(error!) } return error == nil } @@ -91,35 +90,35 @@ public class KeyManager: NSObject { * Returns a JWK for a particular secret key by key id. */ public static func getJwk(id: String) -> String? { - guard let key = getSecretKey(id: id) else { return nil } + guard let key = getSecretKey(id: id) else { return nil } - guard let publicKey = SecKeyCopyPublicKey(key) else { - return nil - } + guard let publicKey = SecKeyCopyPublicKey(key) else { + return nil + } - var error: Unmanaged? - guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else { - return nil - } + var error: Unmanaged? + guard let data = SecKeyCopyExternalRepresentation(publicKey, &error) as? Data else { + return nil + } - let fullData: Data = data.subdata(in: 1 ..< data.count) - let xDataRaw: Data = fullData.subdata(in: 0 ..< 32) - let yDataRaw: Data = fullData.subdata(in: 32 ..< 64) + let fullData: Data = data.subdata(in: 1..? guard let derSignature = SecKeyCreateSignature(secKey, .ecdsaSignatureMessageX962SHA256, payload as CFData, - &error) as Data? - else { - callback.update(state: .error(.generic("Failed to sign message: \(error.debugDescription)"))) - cancel() + &error) as Data? else { + self.callback.update(state: .error(.generic("Failed to sign message: \(error.debugDescription)"))) + self.cancel() return } let response = try SpruceIDMobileSdkRs.submitSignature(sessionManager: sessionManager!, - derSignature: derSignature) - bleManager.writeOutgoingValue(data: response) + derSignature: derSignature) + self.bleManager.writeOutgoingValue(data: response) } catch { - callback.update(state: .error(.generic("\(error)"))) - cancel() + self.callback.update(state: .error(.generic("\(error)"))) + self.cancel() } } } @@ -114,23 +113,23 @@ extension BLESessionManager: MDocBLEDelegate { func callback(message: MDocBLECallback) { switch message { case .done: - callback.update(state: .success) + self.callback.update(state: .success) case .connected: - callback.update(state: .connected) - case let .uploadProgress(value, total): - callback.update(state: .uploadProgress(value, total)) - case let .message(data): + self.callback.update(state: .connected) + case .uploadProgress(let value, let total): + self.callback.update(state: .uploadProgress(value, total)) + case .message(let data): do { - let requestData = try SpruceIDMobileSdkRs.handleRequest(state: state, request: data) - sessionManager = requestData.sessionManager - callback.update(state: .selectNamespaces(requestData.itemsRequests)) + let requestData = try SpruceIDMobileSdkRs.handleRequest(state: self.state, request: data) + self.sessionManager = requestData.sessionManager + self.callback.update(state: .selectNamespaces(requestData.itemsRequests)) } catch { - callback.update(state: .error(.generic("\(error)"))) - cancel() + self.callback.update(state: .error(.generic("\(error)"))) + self.cancel() } - case let .error(error): - callback.update(state: .error(BleSessionError(holderBleError: error))) - cancel() + case .error(let error): + self.callback.update(state: .error(BleSessionError(holderBleError: error))) + self.cancel() } } } @@ -145,9 +144,9 @@ public enum BleSessionError { init(holderBleError: MdocHolderBleError) { switch holderBleError { - case let .peripheral(string): + case .peripheral(let string): self = .peripheral(string) - case let .bluetooth(string): + case .bluetooth(let string): self = .bluetooth(string) } } diff --git a/Sources/MobileSdk/PlatformContext.swift b/Sources/MobileSdk/PlatformContext.swift index bcafb31..f4d1039 100644 --- a/Sources/MobileSdk/PlatformContext.swift +++ b/Sources/MobileSdk/PlatformContext.swift @@ -18,8 +18,8 @@ import Foundation // A container for platform-specific subsystems. class SpruceKitPlatformContext: NSObject { - let keyMgr = KeyManager() // Keys - let storageMgr = StorageManager() // Secure storage. + let keyMgr = KeyManager() // Keys + let storageMgr = StorageManager() // Secure storage. } // diff --git a/Sources/MobileSdk/StorageManager.swift b/Sources/MobileSdk/StorageManager.swift index bf128bf..daeb404 100644 --- a/Sources/MobileSdk/StorageManager.swift +++ b/Sources/MobileSdk/StorageManager.swift @@ -31,9 +31,9 @@ class StorageManager: NSObject, StorageManagerInterface { let bundle = Bundle.main let asdir = try fileman.url(for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, // Ignored - create: true) // May not exist, make if necessary. + in: .userDomainMask, + appropriateFor: nil, // Ignored + create: true) // May not exist, make if necessary. // If we create subdirectories in the application support directory, we need to put them in a subdir // named after the app; normally, that's `CFBundleDisplayName` from `info.plist`, but that key doesn't diff --git a/Sources/MobileSdk/W3CVC.swift b/Sources/MobileSdk/W3CVC.swift index 5596244..f9516a1 100644 --- a/Sources/MobileSdk/W3CVC.swift +++ b/Sources/MobileSdk/W3CVC.swift @@ -13,13 +13,13 @@ public class W3CVC: Credential { if let data = credentialString.data(using: .utf8) { do { let json = try JSONDecoder().decode(GenericJSON.self, from: data) - credential = json + self.credential = json super.init(id: json["id"]!.toString()) } catch let error as NSError { throw error } } else { - credential = nil + self.credential = nil super.init(id: "") throw W3CError.initializationError("Failed to process credential string.") } @@ -31,5 +31,6 @@ public class W3CVC: Credential { } else { return [:] } + } } From 789b2ae545231237427f378ceb79ffff604a12bf Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Thu, 5 Sep 2024 13:52:33 -0400 Subject: [PATCH 15/17] Rename BLEInternalConnection to BLEInternalL2CAPConnection --- Sources/MobileSdk/BLEConnection.swift | 2 +- Sources/MobileSdk/MDocHolderBLECentralConnection.swift | 2 +- Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/MobileSdk/BLEConnection.swift b/Sources/MobileSdk/BLEConnection.swift index b86975b..8a45c9e 100644 --- a/Sources/MobileSdk/BLEConnection.swift +++ b/Sources/MobileSdk/BLEConnection.swift @@ -4,7 +4,7 @@ import CoreBluetooth import Foundation /// The base BLE connection, only intended for subclassing. -class BLEInternalConnection: NSObject, StreamDelegate { +class BLEInternalL2CAPConnection: NSObject, StreamDelegate { var channel: CBL2CAPChannel? private var outputData = Data() diff --git a/Sources/MobileSdk/MDocHolderBLECentralConnection.swift b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift index fc6b81a..34d6f87 100644 --- a/Sources/MobileSdk/MDocHolderBLECentralConnection.swift +++ b/Sources/MobileSdk/MDocHolderBLECentralConnection.swift @@ -10,7 +10,7 @@ public protocol MDocHolderBLECentralConnectionDelegate: AnyObject { func connectionEnd() } -class MDocHolderBLECentralConnection: BLEInternalConnection { +class MDocHolderBLECentralConnection: BLEInternalL2CAPConnection { private let controlDelegate: MDocHolderBLECentralConnectionDelegate /// Initialize a reader peripheral connection. diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift index e9e5bc4..7542556 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheralConnection.swift @@ -9,7 +9,7 @@ protocol MDocReaderBLEPeriConnDelegate: AnyObject { func receivedData(_ data: Data) } -class MDocReaderBLEPeripheralConnection: BLEInternalConnection { +class MDocReaderBLEPeripheralConnection: BLEInternalL2CAPConnection { private let controlDelegate: MDocReaderBLEPeriConnDelegate /// Initialize a reader peripheral connection. From 4b24e110aaee2645c58a90ed2d3caf81d0942fa8 Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Fri, 6 Sep 2024 11:30:10 -0400 Subject: [PATCH 16/17] Export the useL2CAP booleans --- Sources/MobileSdk/BLEConnection.swift | 10 +++++++--- Sources/MobileSdk/Credentials.swift | 8 ++++++-- Sources/MobileSdk/MDoc.swift | 8 ++++++-- Sources/MobileSdk/MDocHolderBLECentral.swift | 5 +++-- Sources/MobileSdk/MDocReader.swift | 6 ++++-- Sources/MobileSdk/MDocReaderBLEPeripheral.swift | 16 +++++++++++++--- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/Sources/MobileSdk/BLEConnection.swift b/Sources/MobileSdk/BLEConnection.swift index 8a45c9e..2dd21ef 100644 --- a/Sources/MobileSdk/BLEConnection.swift +++ b/Sources/MobileSdk/BLEConnection.swift @@ -8,6 +8,7 @@ class BLEInternalL2CAPConnection: NSObject, StreamDelegate { var channel: CBL2CAPChannel? private var outputData = Data() + private var outputDelivered = false private var incomingData = Data() private var incomingTime = Date(timeIntervalSinceNow: 0) private var incomingDelivered = false @@ -52,9 +53,12 @@ class BLEInternalL2CAPConnection: NSObject, StreamDelegate { /// Public send() interface. public func send(data: Data) { - outputData.append(data) - totalBytesWritten = 0 - send() + if !outputDelivered { + outputDelivered = true + outputData = data + totalBytesWritten = 0 + send() + } } /// Internal send() interface. diff --git a/Sources/MobileSdk/Credentials.swift b/Sources/MobileSdk/Credentials.swift index 3fd16dd..62fece5 100644 --- a/Sources/MobileSdk/Credentials.swift +++ b/Sources/MobileSdk/Credentials.swift @@ -9,11 +9,15 @@ public class CredentialStore { // swiftlint:disable force_cast public func presentMdocBLE(deviceEngagement: DeviceEngagement, - callback: BLESessionStateDelegate + callback: BLESessionStateDelegate, + useL2CAP: Bool = true // , trustedReaders: TrustedReaders ) -> BLESessionManager? { if let firstMdoc = self.credentials.first(where: {$0 is MDoc}) { - return BLESessionManager(mdoc: firstMdoc as! MDoc, engagement: DeviceEngagement.QRCode, callback: callback) + return BLESessionManager(mdoc: firstMdoc as! MDoc, + engagement: DeviceEngagement.QRCode, + callback: callback, + useL2CAP: useL2CAP) } else { return nil } diff --git a/Sources/MobileSdk/MDoc.swift b/Sources/MobileSdk/MDoc.swift index 4c8c736..7c29775 100644 --- a/Sources/MobileSdk/MDoc.swift +++ b/Sources/MobileSdk/MDoc.swift @@ -43,16 +43,20 @@ public class BLESessionManager { var sessionManager: SessionManager? var mdoc: MDoc var bleManager: MDocHolderBLECentral! + var useL2CAP: Bool - init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate) { + init?(mdoc: MDoc, engagement: DeviceEngagement, callback: BLESessionStateDelegate, useL2CAP: Bool) { self.callback = callback self.uuid = UUID() self.mdoc = mdoc + self.useL2CAP = useL2CAP do { let sessionData = try SpruceIDMobileSdkRs.initialiseSession(document: mdoc.inner, uuid: self.uuid.uuidString) self.state = sessionData.state - bleManager = MDocHolderBLECentral(callback: self, serviceUuid: CBUUID(nsuuid: self.uuid)) + bleManager = MDocHolderBLECentral(callback: self, + serviceUuid: CBUUID(nsuuid: self.uuid), + useL2CAP: useL2CAP) self.callback.update(state: .engagingQRCode(sessionData.qrCodeUri.data(using: .ascii)!)) } catch { print("\(error)") diff --git a/Sources/MobileSdk/MDocHolderBLECentral.swift b/Sources/MobileSdk/MDocHolderBLECentral.swift index 9dd17e6..66ca533 100644 --- a/Sources/MobileSdk/MDocHolderBLECentral.swift +++ b/Sources/MobileSdk/MDocHolderBLECentral.swift @@ -50,7 +50,7 @@ class MDocHolderBLECentral: NSObject { private var activeStream: MDocHolderBLECentralConnection? /// If this is `false`, we decline to connect to L2CAP even if it is offered. - var useL2CAP = true + var useL2CAP: Bool var machineState = MachineState.initial var machinePendingState = MachineState.initial { @@ -59,9 +59,10 @@ class MDocHolderBLECentral: NSObject { } } - init(callback: MDocBLEDelegate, serviceUuid: CBUUID) { + init(callback: MDocBLEDelegate, serviceUuid: CBUUID, useL2CAP: Bool) { self.serviceUuid = serviceUuid self.callback = callback + self.useL2CAP = useL2CAP super.init() centralManager = CBCentralManager(delegate: self, queue: nil) } diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index 2ec6c2a..a640dd7 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -10,7 +10,8 @@ public class MDocReader { callback: BLEReaderSessionStateDelegate, uri: String, requestedItems: [String: [String: Bool]], - trustAnchorRegistry: [String]? + trustAnchorRegistry: [String]?, + useL2CAP: Bool = true ) { self.callback = callback do { @@ -21,7 +22,8 @@ public class MDocReader { self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, - bleIdent: sessionData.bleIdent) + bleIdent: sessionData.bleIdent, + useL2CAP: useL2CAP) } catch { print("\(error)") return nil diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index aa77725..af7cb0d 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -28,6 +28,7 @@ class MDocReaderBLEPeripheral: NSObject { var identCharacteristic: CBMutableCharacteristic? var l2capCharacteristic: CBMutableCharacteristic? var requestData: Data + var requestSent = false var maximumCharacteristicSize: Int? var writingQueueTotalChunks: Int? var writingQueueChunkIndex: Int? @@ -37,7 +38,7 @@ class MDocReaderBLEPeripheral: NSObject { /// If this is `true`, we offer an L2CAP characteristic and set up an L2CAP stream. If it is `false` we do neither /// of these things, and use the old flow. - var useL2CAP = true + var useL2CAP: Bool private var channelPSM: UInt16? { didSet { @@ -52,10 +53,11 @@ class MDocReaderBLEPeripheral: NSObject { } } - init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data) { + init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data, useL2CAP: Bool) { self.serviceUuid = serviceUuid self.callback = callback self.bleIdent = bleIdent + self.useL2CAP = useL2CAP requestData = request incomingMessageBuffer = Data() super.init() @@ -127,7 +129,15 @@ class MDocReaderBLEPeripheral: NSObject { } case .l2capStreamOpen: // An L2CAP stream is opened. - activeStream?.send(data: requestData) + if !requestSent { + // We occasionally seem to get a transient condition where the stream gets opened + // more than once; locally, I've seen the stream open 10 times in a row, and with + // the non-framed transport for data, that means we send the request 10 times, the + // far side reads that as a single request, and errors out. The requestSent bool + // is to keep this from happening. + activeStream?.send(data: requestData) + requestSent = true + } machineState = .l2capSendingRequest machinePendingState = .l2capSendingRequest update = true From b03e4bd322a31d29d3ff94be3c4c980169c0cdb1 Mon Sep 17 00:00:00 2001 From: Todd Showalter Date: Fri, 6 Sep 2024 11:54:16 -0400 Subject: [PATCH 17/17] Don't export useL2CAP for the reader. --- Sources/MobileSdk/MDocReader.swift | 6 ++---- Sources/MobileSdk/MDocReaderBLEPeripheral.swift | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Sources/MobileSdk/MDocReader.swift b/Sources/MobileSdk/MDocReader.swift index a640dd7..2ec6c2a 100644 --- a/Sources/MobileSdk/MDocReader.swift +++ b/Sources/MobileSdk/MDocReader.swift @@ -10,8 +10,7 @@ public class MDocReader { callback: BLEReaderSessionStateDelegate, uri: String, requestedItems: [String: [String: Bool]], - trustAnchorRegistry: [String]?, - useL2CAP: Bool = true + trustAnchorRegistry: [String]? ) { self.callback = callback do { @@ -22,8 +21,7 @@ public class MDocReader { self.bleManager = MDocReaderBLEPeripheral(callback: self, serviceUuid: CBUUID(string: sessionData.uuid), request: sessionData.request, - bleIdent: sessionData.bleIdent, - useL2CAP: useL2CAP) + bleIdent: sessionData.bleIdent) } catch { print("\(error)") return nil diff --git a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift index af7cb0d..d05833f 100644 --- a/Sources/MobileSdk/MDocReaderBLEPeripheral.swift +++ b/Sources/MobileSdk/MDocReaderBLEPeripheral.swift @@ -38,7 +38,7 @@ class MDocReaderBLEPeripheral: NSObject { /// If this is `true`, we offer an L2CAP characteristic and set up an L2CAP stream. If it is `false` we do neither /// of these things, and use the old flow. - var useL2CAP: Bool + var useL2CAP = true private var channelPSM: UInt16? { didSet { @@ -53,11 +53,10 @@ class MDocReaderBLEPeripheral: NSObject { } } - init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data, useL2CAP: Bool) { + init(callback: MDocReaderBLEDelegate, serviceUuid: CBUUID, request: Data, bleIdent: Data) { self.serviceUuid = serviceUuid self.callback = callback self.bleIdent = bleIdent - self.useL2CAP = useL2CAP requestData = request incomingMessageBuffer = Data() super.init()