Skip to content

Commit

Permalink
Add MDoc Reader support (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
sbihel authored Sep 4, 2024
1 parent 1ed7676 commit 8b80370
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 7 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/spruceid/mobile-sdk-rs.git",
"state" : {
"revision" : "9deb085da5d26630045505cad6f46f11460d739d",
"version" : "0.0.27"
"revision" : "e8a1b7056989e4cb471281b464c1aeac298be97f",
"version" : "0.0.29"
}
},
{
Expand Down
3 changes: 1 addition & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ 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.28"),
.package(url: "https://github.com/spruceid/mobile-sdk-rs.git", from: "0.0.29"),
// .package(path: "../mobile-sdk-rs"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0")
],
Expand Down
21 changes: 21 additions & 0 deletions Sources/MobileSdk/MDocBLEUtils.swift
Original file line number Diff line number Diff line change
@@ -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")
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: 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/MobileSdk/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
97 changes: 97 additions & 0 deletions Sources/MobileSdk/MDocReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import CoreBluetooth
import SpruceIDMobileSdkRs

public class MDocReader {
var sessionManager: MdlSessionManager
var bleManager: MDocReaderBLEPeripheral!
var callback: BLEReaderSessionStateDelegate

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: trustAnchorRegistry)
self.sessionManager = sessionData.state
self.bleManager = MDocReaderBLEPeripheral(callback: self,
serviceUuid: CBUUID(string: sessionData.uuid),
request: sessionData.request,
bleIdent: sessionData.bleIdent)
} 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 SpruceIDMobileSdkRs.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: 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/MobileSdk/MDocReaderBLEPeripheral.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import Algorithms
import CoreBluetooth
import Foundation
import SpruceIDMobileSdkRs

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)
self.stateCharacteristic = CBMutableCharacteristic(type: readerStateCharacteristicId,
properties: [.notify, .writeWithoutResponse, .write],
value: nil,
permissions: [.writeable])
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.identCharacteristic = CBMutableCharacteristic(type: readerIdentCharacteristicId,
properties: [.read],
value: bleIdent,
permissions: [.readable])
// 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)
}
}
}
}
Loading

0 comments on commit 8b80370

Please sign in to comment.