diff --git a/Package.resolved b/Package.resolved index b6c0d64..f00f0e1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.1.0" } }, + { + "identity" : "blueswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/conanoc/BlueSwift", + "state" : { + "revision" : "5c9cc238396061bf7f3d6056dc8fdfe060a1add3", + "version" : "1.1.7" + } + }, { "identity" : "cocoaasyncsocket", "kind" : "remoteSourceControl", @@ -99,6 +108,15 @@ "version" : "1.2.2" } }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, { "identity" : "swift-bases", "kind" : "remoteSourceControl", @@ -116,6 +134,15 @@ "revision" : "45f3cf2844477b9d211e1d3e793d0853134fd942", "version" : "0.0.2" } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 486238d..c6fe4f8 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,9 @@ let package = Package( .package(url: "https://github.com/keefertaylor/Base58Swift", exact: "2.1.7"), .package(url: "https://github.com/thecatalinstan/Criollo", exact: "1.1.0"), .package(url: "https://github.com/groue/Semaphore", exact: "0.0.8"), - .package(url: "https://github.com/beatt83/peerdid-swift", exact: "3.0.0") + .package(url: "https://github.com/beatt83/peerdid-swift", exact: "3.0.0"), + .package(url: "https://github.com/apple/swift-algorithms", exact: "1.2.0"), + .package(url: "https://github.com/conanoc/BlueSwift", exact: "1.1.7") ], targets: [ .target( @@ -30,9 +32,11 @@ let package = Package( .product(name: "IndyVdr", package: "aries-uniffi-wrappers"), .product(name: "WebSockets", package: "concurrent-ws"), .product(name: "PeerDID", package: "peerdid-swift"), + .product(name: "Algorithms", package: "swift-algorithms"), "CollectionConcurrencyKit", "Base58Swift", - "Semaphore" + "Semaphore", + "BlueSwift" ]), .testTarget( name: "AriesFrameworkTests", diff --git a/README.md b/README.md index d5b81f7..ee1b4c2 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Aries Framework Swift supports most of [AIP 1.0](https://github.com/hyperledger/ - ✅ ([RFC 0036](https://github.com/hyperledger/aries-rfcs/blob/master/features/0036-issue-credential/README.md)) Issue Credential Protocol - ✅ ([RFC 0037](https://github.com/hyperledger/aries-rfcs/tree/master/features/0037-present-proof/README.md)) Present Proof Protocol - Does not implement alternate begining (Prover begins with proposal) -- ✅ HTTP & WebSocket Transport +- ✅ HTTP, WebSocket and Bluetooth Transport - ✅ ([RFC 0434](https://github.com/hyperledger/aries-rfcs/blob/main/features/0434-outofband/README.md)) Out of Band Protocol (AIP 2.0) - ✅ ([RFC 0035](https://github.com/hyperledger/aries-rfcs/blob/main/features/0035-report-problem/README.md)) Report Problem Protocol - ✅ ([RFC 0023](https://github.com/hyperledger/aries-rfcs/tree/main/features/0023-did-exchange)) DID Exchange Protocol (AIP 2.0) @@ -28,7 +28,7 @@ Aries Framework Swift requires iOS 15.0+ and distributed as a Swift package. Add a dependency to your `Package.swift` file: ```swift dependencies: [ - .package(url: "https://github.com/hyperledger/aries-framework-swift", from: "2.3.0") + .package(url: "https://github.com/hyperledger/aries-framework-swift", from: "2.5.0") ] ``` @@ -140,6 +140,27 @@ Another way to handle those requests is to implement your own `MessageHandler` c agent.dispatcher.registerHandler(handler: messageHandler) ``` +## Bluetooth support + +Aries Framework Swift supports phone to phone communication over Bluetooth. +You will need to add `NSBluetoothAlwaysUsageDescription` key to the info.plist of your app to use Bluetooth. + +### How to use + +Verifier side: +1. Call `try await agent.startBLE()` to create an endpoint over BLE. The endpoint has the form of "ble://aries/endpoint?uuid={uuid}". +2. Create an oob-invitation and create a QR code with the invitation url. This invitation will use the endpoint created above even though the agent has a mediator connection. You should create an oob-invitation attaching a proof request message without handshake option. This allows the prover sends the proof directly to the verifier without preparing any endpoint. +```swift +let oob = try await agent!.oob.createInvitation(config: CreateOutOfBandInvitationConfig(handshake: false, messages: [message])) +let invitationUrl = oob.outOfBandInvitation.toUrl(domain: "http://example.com") +``` +3. Call `try? await agent.stopBLE()` after you finish verification. + +Prover side: +- There is nothing you need to do to communicate over BLE on prover side. The agent will recognize the `ble://` scheme and connect to the verifier's device over BLE. The connection will be closed automatically after the message is sent. + +The sample app has sample codes that demonstrates proof exchange over Bluetooth. + ## Sample App `Sample` directory contains an iOS sample app that demonstrates how to use Aries Framework Swift. The app receives a connection invitation from a QR code or from a URL input and handles credential offers and proof requests. diff --git a/Sample/wallet-app-ios.xcodeproj/project.pbxproj b/Sample/wallet-app-ios.xcodeproj/project.pbxproj index 2c83516..77347b8 100644 --- a/Sample/wallet-app-ios.xcodeproj/project.pbxproj +++ b/Sample/wallet-app-ios.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ BB90F26828F92DA20066AE87 /* local-genesis.txn in Resources */ = {isa = PBXBuildFile; fileRef = BB90F26628F92DA10066AE87 /* local-genesis.txn */; }; BBB509D628ED680700A6405A /* WalletOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB509D528ED680700A6405A /* WalletOpener.swift */; }; BBB509D828ED682000A6405A /* OpenWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB509D728ED681F00A6405A /* OpenWalletView.swift */; }; + BBB9FD232C2D4F1B008FD38D /* RequestProofView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB9FD222C2D4F1B008FD38D /* RequestProofView.swift */; }; BBD7B25127181C4300C3FB6C /* QRCodeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBD7B25027181C4300C3FB6C /* QRCodeHandler.swift */; }; /* End PBXBuildFile section */ @@ -37,6 +38,7 @@ BB90F26628F92DA10066AE87 /* local-genesis.txn */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "local-genesis.txn"; sourceTree = ""; }; BBB509D528ED680700A6405A /* WalletOpener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletOpener.swift; sourceTree = ""; }; BBB509D728ED681F00A6405A /* OpenWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenWalletView.swift; sourceTree = ""; }; + BBB9FD222C2D4F1B008FD38D /* RequestProofView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestProofView.swift; sourceTree = ""; }; BBD7B25027181C4300C3FB6C /* QRCodeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeHandler.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,6 +96,7 @@ BB5C4680270C1501008D77AE /* WalletMainView.swift */, BBB509D528ED680700A6405A /* WalletOpener.swift */, BBB509D728ED681F00A6405A /* OpenWalletView.swift */, + BBB9FD222C2D4F1B008FD38D /* RequestProofView.swift */, BB5C468C270C390E008D77AE /* CredentialListView.swift */, BB5C468E270C4181008D77AE /* CredentialDetailView.swift */, BBD7B25027181C4300C3FB6C /* QRCodeHandler.swift */, @@ -202,6 +205,7 @@ files = ( BBB509D828ED682000A6405A /* OpenWalletView.swift in Sources */, BBD7B25127181C4300C3FB6C /* QRCodeHandler.swift in Sources */, + BBB9FD232C2D4F1B008FD38D /* RequestProofView.swift in Sources */, BB5C468D270C390E008D77AE /* CredentialListView.swift in Sources */, BB5C4681270C1501008D77AE /* WalletMainView.swift in Sources */, BB5C468F270C4181008D77AE /* CredentialDetailView.swift in Sources */, @@ -336,13 +340,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"wallet-app-ios/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = RAZ4DT76QQ; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Need bluetooth for proof exchange"; INFOPLIST_KEY_NSCameraUsageDescription = "Use camera to scan QR Code"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -356,7 +361,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "org.hyperledger.aries.demo.wallet-app-ios"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hyperledger.aries.demo.wallet-app-ios2"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; @@ -371,13 +376,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"wallet-app-ios/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = RAZ4DT76QQ; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Need bluetooth for proof exchange"; INFOPLIST_KEY_NSCameraUsageDescription = "Use camera to scan QR Code"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; @@ -391,7 +397,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "org.hyperledger.aries.demo.wallet-app-ios"; + PRODUCT_BUNDLE_IDENTIFIER = "org.hyperledger.aries.demo.wallet-app-ios2"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Sample/wallet-app-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Sample/wallet-app-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3931e5a..23a695f 100644 --- a/Sample/wallet-app-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Sample/wallet-app-ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/hyperledger/aries-uniffi-wrappers", "state" : { - "revision" : "f335e392d6c5b8bd4dfa65746f214b80cdc60551", - "version" : "0.2.0" + "revision" : "01def706b44a4095032cd42f0480a5bdfb809961", + "version" : "0.2.1" } }, { @@ -27,6 +27,15 @@ "version" : "3.1.0" } }, + { + "identity" : "blueswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/conanoc/BlueSwift", + "state" : { + "revision" : "5c9cc238396061bf7f3d6056dc8fdfe060a1add3", + "version" : "1.1.7" + } + }, { "identity" : "cocoaasyncsocket", "kind" : "remoteSourceControl", @@ -72,6 +81,24 @@ "version" : "1.1.0" } }, + { + "identity" : "didcore-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/beatt83/didcore-swift.git", + "state" : { + "revision" : "2503b1690e11b16a0cdc8f492e370bbd9dcbe08b", + "version" : "2.0.0" + } + }, + { + "identity" : "peerdid-swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/beatt83/peerdid-swift", + "state" : { + "revision" : "4e82ff42aa2b53b2d361f482b607763d79ccfdbb", + "version" : "3.0.0" + } + }, { "identity" : "semaphore", "kind" : "remoteSourceControl", @@ -89,6 +116,42 @@ "revision" : "e325083424688055363bbfcb7f1a440d7d7a1bae", "version" : "1.2.2" } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-bases", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-libp2p/swift-bases.git", + "state" : { + "revision" : "3cf27cf95d70248b0a1d99eee06cdf8b235241a8", + "version" : "0.0.3" + } + }, + { + "identity" : "swift-multibase", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-libp2p/swift-multibase.git", + "state" : { + "revision" : "45f3cf2844477b9d211e1d3e793d0853134fd942", + "version" : "0.0.2" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } } ], "version" : 2 diff --git a/Sample/wallet-app-ios/CredentialHandler.swift b/Sample/wallet-app-ios/CredentialHandler.swift index d7ae54b..60327b8 100644 --- a/Sample/wallet-app-ios/CredentialHandler.swift +++ b/Sample/wallet-app-ios/CredentialHandler.swift @@ -37,6 +37,12 @@ extension CredentialHandler: AgentDelegate { } else if proofRecord.state == .Done { menu = nil showSimpleAlert(message: "Proof done") + } else if proofRecord.state == .PresentationReceived { + menu = nil + showSimpleAlert(message: "Proof.isVerified: \(proofRecord.isVerified!)") + } else if proofRecord.state == .PresentationSent { + menu = nil + showSimpleAlert(message: "Proof sent") } } } @@ -88,7 +94,7 @@ extension CredentialHandler: AgentDelegate { _ = try await agent!.proofs.acceptRequest(proofRecordId: proofRecordId, requestedCredentials: requestedCredentials) } catch { menu = nil - showSimpleAlert(message: "Failed to present proof") + showSimpleAlert(message: "Failed to present proof: \(error)") print(error) } } @@ -110,4 +116,15 @@ extension CredentialHandler: AgentDelegate { self?.showAlert = true } } + + func createProofInvitation() async throws -> String { + let attributes = ["attrbutes1": ProofAttributeInfo(names: ["name", "degree"])] + let nonce = try ProofService.generateProofRequestNonce() + let proofRequest = ProofRequest(nonce: nonce, requestedAttributes: attributes, requestedPredicates: [:]) + let (message, _) = try await agent!.proofService.createRequest(proofRequest: proofRequest) + let outOfBandRecord = try await agent!.oob.createInvitation( + config: CreateOutOfBandInvitationConfig(handshake: false, messages: [message])) + let invitation = outOfBandRecord.outOfBandInvitation + return try invitation.toUrl(domain: "http://example.com") + } } diff --git a/Sample/wallet-app-ios/QRCodeHandler.swift b/Sample/wallet-app-ios/QRCodeHandler.swift index 9425196..eca6fc2 100644 --- a/Sample/wallet-app-ios/QRCodeHandler.swift +++ b/Sample/wallet-app-ios/QRCodeHandler.swift @@ -13,7 +13,7 @@ class QRCodeHandler { Task { do { let (_, connection) = try await agent!.oob.receiveInvitationFromUrl(url) - await credentialHandler.showSimpleAlert(message: "Connected with \(connection?.theirLabel ?? "unknown agent")") + print("Connected with \(connection?.theirLabel ?? "unknown agent")") } catch { print(error) await credentialHandler.reportError() diff --git a/Sample/wallet-app-ios/RequestProofView.swift b/Sample/wallet-app-ios/RequestProofView.swift new file mode 100644 index 0000000..53157f8 --- /dev/null +++ b/Sample/wallet-app-ios/RequestProofView.swift @@ -0,0 +1,56 @@ +// +// RequestProofView.swift +// wallet-app-ios +// + +import SwiftUI +import CoreImage.CIFilterBuiltins + +struct RequestProofView: View { + @State var qrReady: Bool = false + @State var invitation = "hello world!" + let credentialHandler = CredentialHandler.shared + let context = CIContext() + let filter = CIFilter.qrCodeGenerator() + + var body: some View { + VStack { + Image(uiImage: generateQRCode(from: invitation)) + .resizable() + .scaledToFit() + if !qrReady { + ProgressView() + } + } + .task { + do { + try await agent!.startBLE() + invitation = try await credentialHandler.createProofInvitation() + qrReady = true + } catch { + print("Failed to create QR: \(error)") + } + } + .onDisappear() { + Task { + try? await agent!.stopBLE() + } + } + } + + func generateQRCode(from string: String) -> UIImage { + filter.message = Data(string.utf8) + + if let outputImage = filter.outputImage { + if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) { + return UIImage(cgImage: cgImage) + } + } + + return UIImage(systemName: "xmark.circle") ?? UIImage() + } +} + +#Preview { + RequestProofView() +} diff --git a/Sample/wallet-app-ios/WalletMainView.swift b/Sample/wallet-app-ios/WalletMainView.swift index 3867635..c194e25 100644 --- a/Sample/wallet-app-ios/WalletMainView.swift +++ b/Sample/wallet-app-ios/WalletMainView.swift @@ -7,7 +7,7 @@ import SwiftUI import CodeScanner enum MainMenu: Identifiable { - case qrcode, list, loading + case qrcode, list, loading, request var id: Int { hashValue } @@ -33,6 +33,12 @@ struct WalletMainView: View { }) { Text("Credentials") } + + Button(action: { + credentialHandler.menu = .request + }) { + Text("Request a proof") + } } .navigationTitle("Wallet App") .listStyle(.plain) @@ -61,6 +67,8 @@ struct WalletMainView: View { CodeScannerView(codeTypes: [.qr], completion: QRCodeHandler().handleResult) case .list: CredentialListView() + case .request: + RequestProofView() case .loading: Text("Processing ...") } diff --git a/Sample/wallet-app-ios/WalletOpener.swift b/Sample/wallet-app-ios/WalletOpener.swift index 27296cc..3d96c0b 100644 --- a/Sample/wallet-app-ios/WalletOpener.swift +++ b/Sample/wallet-app-ios/WalletOpener.swift @@ -40,7 +40,7 @@ class WalletOpener : ObservableObject { autoAcceptProof: .never) do { - agent = Agent(agentConfig: config, agentDelegate: CredentialHandler.shared) + agent = Agent(agentConfig: config, agentDelegate: await CredentialHandler.shared) try await agent!.initialize() } catch { print("Cannot initialize agent: \(error)") diff --git a/Sources/AriesFramework/agent/Agent.swift b/Sources/AriesFramework/agent/Agent.swift index ca5910c..fc8a193 100644 --- a/Sources/AriesFramework/agent/Agent.swift +++ b/Sources/AriesFramework/agent/Agent.swift @@ -38,6 +38,12 @@ public class Agent { public var wallet: Wallet! private var _isInitialized = false + var bleInboundTransport: BleInboundTransport! + public var isBluetoothOn: Bool { + return _isBluetoothOn + } + private var _isBluetoothOn = false + public init(agentConfig: AgentConfig, agentDelegate: AgentDelegate?) { self.agentConfig = agentConfig self.agentDelegate = agentDelegate @@ -69,6 +75,7 @@ public class Agent { self.proofRepository = ProofRepository(agent: self) self.proofService = ProofService(agent: self) self.proofs = ProofCommand(agent: self, dispatcher: self.dispatcher) + self.bleInboundTransport = BleInboundTransport(agent: self) } /** @@ -150,4 +157,28 @@ public class Agent { public static func generateWalletKey() throws -> String { return try AskarStoreManager().generateRawStoreKey(seed: nil) } + + /** + Start the BLE inbound transport. This enables message exchange via Bluetooth. + Call this before creating a connection invitation. + Note that the BLE outbound transport is available regardless of the state of BLE. + */ + public func startBLE() async throws { + if _isBluetoothOn { + return + } + try await bleInboundTransport.start() + _isBluetoothOn = true + } + + /** + Stop the BLE inbound transport. + */ + public func stopBLE() async throws { + if !_isBluetoothOn { + return + } + try await bleInboundTransport.stop() + _isBluetoothOn = false + } } diff --git a/Sources/AriesFramework/agent/MessageSender.swift b/Sources/AriesFramework/agent/MessageSender.swift index af7b287..6bb7a0b 100644 --- a/Sources/AriesFramework/agent/MessageSender.swift +++ b/Sources/AriesFramework/agent/MessageSender.swift @@ -7,12 +7,14 @@ public class MessageSender { var defaultOutboundTransport: OutboundTransport? let httpOutboundTransport: HttpOutboundTransport let wsOutboundTransport: WsOutboundTransport + let bleOutboundTransport: BleOutboundTransport let logger = Logger(subsystem: "AriesFramework", category: "MessageSender") public init(agent: Agent) { self.agent = agent self.httpOutboundTransport = HttpOutboundTransport(agent) self.wsOutboundTransport = WsOutboundTransport(agent) + self.bleOutboundTransport = BleOutboundTransport(agent) } public func setOutboundTransport(_ outboundTransport: OutboundTransport) { @@ -26,6 +28,8 @@ public class MessageSender { return httpOutboundTransport } else if endpoint.hasPrefix("ws://") || endpoint.hasPrefix("wss://") { return wsOutboundTransport + } else if endpoint.hasPrefix("ble://") { + return bleOutboundTransport } else { return nil } @@ -69,7 +73,7 @@ public class MessageSender { } logger.debug("Send outbound message of type \(agentMessage.type) to endpoint \(service.serviceEndpoint)") if endpointPrefix == nil && outboundTransportForEndpoint(service.serviceEndpoint) == nil { - logger.debug("endpoint is not supported") + logger.debug("Endpoint is not supported") continue } do { diff --git a/Sources/AriesFramework/agent/transport/BleInboundTransport.swift b/Sources/AriesFramework/agent/transport/BleInboundTransport.swift new file mode 100644 index 0000000..4d2cd31 --- /dev/null +++ b/Sources/AriesFramework/agent/transport/BleInboundTransport.swift @@ -0,0 +1,72 @@ + +import Foundation +import os +import BlueSwift + +public class BleInboundTransport: InboundTransport { + let logger = Logger(subsystem: "AriesFramework", category: "BleInboundTransport") + let agent: Agent + let advertisement = BluetoothAdvertisement.shared + var receivedMessage = Data() + var uuid = "" + /// The UUID used for BLE service and characteristic + public var identifier: String { + return uuid + } + + init(agent: Agent) { + self.agent = agent + } + + public func start() async throws { + uuid = UUID().uuidString + let characteristic = try Characteristic(uuid: uuid) + let service = try Service(uuid: uuid, characteristics: [characteristic]) + let configuration = try Configuration(services: [service], advertisement: uuid) + let peripheral = Peripheral(configuration: configuration, advertisementData: [.servicesUUIDs(uuid)]) + + let error = await withCheckedContinuation({ continuation in + advertisement.advertise(peripheral: peripheral) { error in + continuation.resume(returning: error) + } + }) + if error != nil { + throw AriesFrameworkError.frameworkError("BLE advertisement failed: \(error!)") + } + logger.debug("BLE advertisement started!") + + advertisement.writeRequestCallback = { [weak self] characteristic, data in + guard let data = data else { return } + do { + if String(data: data, encoding: .utf8) == BleOutboundTransport.EOF && self != nil { + let encryptedMessage = try JSONDecoder().decode(EncryptedMessage.self, from: self!.receivedMessage) + Task { [weak self] in + try await self?.agent.receiveMessage(encryptedMessage) + } + self?.receivedMessage = Data() + } else { + self?.receivedMessage.append(data) + } + } catch { + self?.logger.error("Error receiving message via BLE: \(error)") + } + } + } + + public func stop() async throws { + uuid = "" + advertisement.stopAdvertising() + logger.debug("BLE advertisement stoped") + } + + public func endpoint(domain: String = "aries/endpoint") throws -> String { + if uuid == "" { + throw AriesFrameworkError.frameworkError("BleInboundTransport is not started yet") + } + return BleInboundTransport.urlFromUUID(uuid, domain: domain) + } + + public static func urlFromUUID(_ uuid: String, domain: String = "aries/endpoint") -> String { + return "ble://\(domain)?uuid=\(uuid)" + } +} diff --git a/Sources/AriesFramework/agent/transport/BleOutboundTransport.swift b/Sources/AriesFramework/agent/transport/BleOutboundTransport.swift new file mode 100644 index 0000000..2730f5d --- /dev/null +++ b/Sources/AriesFramework/agent/transport/BleOutboundTransport.swift @@ -0,0 +1,89 @@ + +import Foundation +import os +import BlueSwift +import Algorithms + +public class BleOutboundTransport: OutboundTransport { + let logger = Logger(subsystem: "AriesFramework", category: "BleOutboundTransport") + let agent: Agent + let central = BluetoothConnection.shared + let chunkSize = 128 + static let EOF = "ARIES_BLE_EOF" + + init(_ agent: Agent) { + self.agent = agent + } + + public func sendPackage(_ package: OutboundPackage) async throws { + logger.debug("Sending outbound message to endpoint \(package.endpoint)") + let uuid = try uuidFromUrl(package.endpoint) + let characteristic = try Characteristic(uuid: uuid) + let service = try Service(uuid: uuid, characteristics: [characteristic]) + let configuration = try Configuration(services: [service], advertisement: uuid) + let peripheral = Peripheral(configuration: configuration) + + let bleWaiter = AsyncWaiter(timeout: 10) + var connectionError: ConnectionError? + central.connect(peripheral) { error in + connectionError = error + bleWaiter.finish() + } + let success = try await bleWaiter.wait() + try validateConnection(success: success, connectionError: connectionError) + + try await writeTo(peripheral: peripheral, characteristic: characteristic, payload: try JSONEncoder().encode(package.payload)) + central.disconnect(peripheral) + } + + func writeTo(peripheral: Peripheral, characteristic: Characteristic, payload: Data) async throws { + let bleWaiter = AsyncWaiter(timeout: 10) + let chunks = payload.chunks(ofCount: chunkSize) + let dataChunks = chunks.map { Data($0) } + for chunk in dataChunks { + let command = Command.data(chunk) + var sendError: Error? + peripheral.write(command: command, characteristic: characteristic) { error in + sendError = error + bleWaiter.finish() + } + let success = try await bleWaiter.wait() + try validateWrite(success: success, sendError: sendError) + } + + let command = Command.utf8String(BleOutboundTransport.EOF) + var sendError: Error? + peripheral.write(command: command, characteristic: characteristic) { error in + sendError = error + bleWaiter.finish() + } + let success = try await bleWaiter.wait() + try validateWrite(success: success, sendError: sendError) + } + + func validateWrite(success: Bool, sendError: Error?) throws { + if !success { + throw AriesFrameworkError.frameworkError("Timeout writing to peripheral") + } + if sendError != nil { + throw AriesFrameworkError.frameworkError("Failed to send message to peripheral: \(String(describing: sendError))") + } + } + + func validateConnection(success: Bool, connectionError: ConnectionError?) throws { + if !success { + throw AriesFrameworkError.frameworkError("Timeout waiting for connection to peripheral") + } + if connectionError != nil { + throw AriesFrameworkError.frameworkError("Failed to connect to peripheral: \(String(describing: connectionError))") + } + } + + func uuidFromUrl(_ url: String) throws -> String { + let queryItems = URLComponents(string: url)?.queryItems + if let uuid = queryItems?.first(where: { $0.name == "uuid" })?.value { + return uuid + } + throw AriesFrameworkError.frameworkError("Invalid url: Cannot find uuid in url \(url)") + } +} diff --git a/Sources/AriesFramework/agent/HttpOutboundTransport.swift b/Sources/AriesFramework/agent/transport/HttpOutboundTransport.swift similarity index 100% rename from Sources/AriesFramework/agent/HttpOutboundTransport.swift rename to Sources/AriesFramework/agent/transport/HttpOutboundTransport.swift diff --git a/Sources/AriesFramework/agent/transport/InboundTransport.swift b/Sources/AriesFramework/agent/transport/InboundTransport.swift new file mode 100644 index 0000000..3b58bf5 --- /dev/null +++ b/Sources/AriesFramework/agent/transport/InboundTransport.swift @@ -0,0 +1,7 @@ + +import Foundation + +public protocol InboundTransport { + func start() async throws + func stop() async throws +} diff --git a/Sources/AriesFramework/agent/OutboundTransport.swift b/Sources/AriesFramework/agent/transport/OutboundTransport.swift similarity index 100% rename from Sources/AriesFramework/agent/OutboundTransport.swift rename to Sources/AriesFramework/agent/transport/OutboundTransport.swift diff --git a/Sources/AriesFramework/agent/SubjectOutboundTransport.swift b/Sources/AriesFramework/agent/transport/SubjectOutboundTransport.swift similarity index 100% rename from Sources/AriesFramework/agent/SubjectOutboundTransport.swift rename to Sources/AriesFramework/agent/transport/SubjectOutboundTransport.swift diff --git a/Sources/AriesFramework/agent/WsOutboundTransport.swift b/Sources/AriesFramework/agent/transport/WsOutboundTransport.swift similarity index 100% rename from Sources/AriesFramework/agent/WsOutboundTransport.swift rename to Sources/AriesFramework/agent/transport/WsOutboundTransport.swift diff --git a/Sources/AriesFramework/oob/models/OutOfBandTypes.swift b/Sources/AriesFramework/oob/models/OutOfBandTypes.swift index 5b92ece..c9521aa 100644 --- a/Sources/AriesFramework/oob/models/OutOfBandTypes.swift +++ b/Sources/AriesFramework/oob/models/OutOfBandTypes.swift @@ -20,6 +20,19 @@ public enum OutOfBandState: String, Codable { } public struct CreateOutOfBandInvitationConfig { + public init(label: String? = nil, alias: String? = nil, imageUrl: String? = nil, goalCode: String? = nil, goal: String? = nil, handshake: Bool? = nil, messages: [AgentMessage]? = nil, multiUseInvitation: Bool? = nil, autoAcceptConnection: Bool? = nil, routing: Routing? = nil) { + self.label = label + self.alias = alias + self.imageUrl = imageUrl + self.goalCode = goalCode + self.goal = goal + self.handshake = handshake + self.messages = messages + self.multiUseInvitation = multiUseInvitation + self.autoAcceptConnection = autoAcceptConnection + self.routing = routing + } + public var label: String? public var alias: String? public var imageUrl: String? @@ -33,6 +46,16 @@ public struct CreateOutOfBandInvitationConfig { } public struct ReceiveOutOfBandInvitationConfig { + public init(label: String? = nil, alias: String? = nil, imageUrl: String? = nil, autoAcceptInvitation: Bool? = nil, autoAcceptConnection: Bool? = nil, reuseConnection: Bool? = nil, routing: Routing? = nil) { + self.label = label + self.alias = alias + self.imageUrl = imageUrl + self.autoAcceptInvitation = autoAcceptInvitation + self.autoAcceptConnection = autoAcceptConnection + self.reuseConnection = reuseConnection + self.routing = routing + } + public var label: String? public var alias: String? public var imageUrl: String? diff --git a/Sources/AriesFramework/oob/repository/OutOfBandRecord.swift b/Sources/AriesFramework/oob/repository/OutOfBandRecord.swift index e1322aa..cea83d2 100644 --- a/Sources/AriesFramework/oob/repository/OutOfBandRecord.swift +++ b/Sources/AriesFramework/oob/repository/OutOfBandRecord.swift @@ -8,7 +8,7 @@ public struct OutOfBandRecord: BaseRecord { var updatedAt: Date? public var tags: Tags? - var outOfBandInvitation: OutOfBandInvitation + public var outOfBandInvitation: OutOfBandInvitation var role: OutOfBandRole var state: OutOfBandState var reusable: Bool diff --git a/Sources/AriesFramework/oob/util/InvitationUrlParser.swift b/Sources/AriesFramework/oob/util/InvitationUrlParser.swift index 3ecaca4..c5338e0 100644 --- a/Sources/AriesFramework/oob/util/InvitationUrlParser.swift +++ b/Sources/AriesFramework/oob/util/InvitationUrlParser.swift @@ -30,7 +30,9 @@ public class InvitationUrlParser { } static func invitationFromShortUrl(_ url: String) async throws -> (OutOfBandInvitation?, ConnectionInvitationMessage?) { - let url = URL(string: url)! + guard let url = URL(string: url) else { + throw AriesFrameworkError.frameworkError("Invalid url: \(url)") + } let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url)) if response.mimeType != "application/json" { throw AriesFrameworkError.frameworkError("Invalid content-type from short url: \(String(describing: response.mimeType))") diff --git a/Sources/AriesFramework/proofs/models/ProofAttributeInfo.swift b/Sources/AriesFramework/proofs/models/ProofAttributeInfo.swift index 865a3dc..5a94fa1 100644 --- a/Sources/AriesFramework/proofs/models/ProofAttributeInfo.swift +++ b/Sources/AriesFramework/proofs/models/ProofAttributeInfo.swift @@ -1,6 +1,13 @@ import Foundation public struct ProofAttributeInfo { + public init(name: String? = nil, names: [String]? = nil, nonRevoked: RevocationInterval? = nil, restrictions: [AttributeFilter]? = nil) { + self.name = name + self.names = names + self.nonRevoked = nonRevoked + self.restrictions = restrictions + } + public let name: String? public let names: [String]? public let nonRevoked: RevocationInterval? diff --git a/Sources/AriesFramework/routing/MediationRecipient.swift b/Sources/AriesFramework/routing/MediationRecipient.swift index a04c29d..258a9cd 100644 --- a/Sources/AriesFramework/routing/MediationRecipient.swift +++ b/Sources/AriesFramework/routing/MediationRecipient.swift @@ -136,6 +136,10 @@ class MediationRecipient { } func getRoutingInfo() async throws -> ([String], [String]) { + if agent.isBluetoothOn { + return ([try agent.bleInboundTransport.endpoint()], []) + } + let mediator = try await repository.getDefault() let endpoints = mediator?.endpoint == nil ? agent.agentConfig.endpoints : [mediator!.endpoint!] let routingKeys = mediator?.routingKeys ?? [] @@ -146,7 +150,7 @@ class MediationRecipient { let (endpoints, routingKeys) = try await getRoutingInfo() let (did, verkey) = try await agent.wallet.createDid() let mediator = try await repository.getDefault() - if mediator != nil && mediator!.isReady() { + if mediator != nil && mediator!.isReady() && !agent.isBluetoothOn { try await keylistUpdate(mediator: mediator!, verkey: verkey) }