Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MDoc Reader support #22

Merged
merged 12 commits into from
Sep 4, 2024
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
Loading