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