Skip to content

Commit

Permalink
Add MDoc Reader support
Browse files Browse the repository at this point in the history
  • Loading branch information
sbihel committed Jul 29, 2024
1 parent 1ea0c66 commit 5f34843
Show file tree
Hide file tree
Showing 7 changed files with 345 additions and 6 deletions.
2 changes: 0 additions & 2 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSCameraUsageDescription</key>
<string>QR Code Scanner</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Secure transmission of mobile DL data</string>
</dict>
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down
21 changes: 21 additions & 0 deletions Sources/WalletSdk/MDocBLEUtils.swift
Original file line number Diff line number Diff line change
@@ -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")
Expand All @@ -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
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions Sources/WalletSdk/MDocHolderBLECentral.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!)
Expand Down
87 changes: 87 additions & 0 deletions Sources/WalletSdk/MDocReader.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
232 changes: 232 additions & 0 deletions Sources/WalletSdk/MDocReaderBLEPeripheral.swift
Original file line number Diff line number Diff line change
@@ -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<ChunksOfCountCollection<Data>>?

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)
}
}
}
}
4 changes: 2 additions & 2 deletions project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 5f34843

Please sign in to comment.