From 65ff99d66d9d44c1cbe133b387ea64c379de09c2 Mon Sep 17 00:00:00 2001 From: goncalo-frade-iohk Date: Tue, 13 Aug 2024 15:44:21 +0100 Subject: [PATCH] feat!(agent): agent separation of concerns BREAKING CHANGE: This is a refactor, from now on the EdgeAgent will not have any reference with DIDComm and a DIDCommAgent will replace this. EdgeAgent now will scope all the logic that is inherent to it removing any transport layer association, and new agents like DIDCommAgent will scope the EdgeAgent functionalities for a transport layer. With this Pollux also has some significant changes so it is not aggregated to the DIDComm Message. OIDCAgent will take part of OIDC transport layer communication. Signed-off-by: goncalo-frade-iohk --- EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift | 41 +++ .../Credentials/ProvableCredential.swift | 20 ++ .../DIDCommAgent+Credentials.swift | 282 ++++++++++++++++++ .../DIDCommAgent/DIDCommAgent+DIDs.swift | 165 ++++++++++ .../DIDCommAgent+Invitations.swift} | 91 +++--- .../DIDCommAgent+Messages.swift} | 7 +- .../DIDCommAgent/DIDCommAgent+Proof.swift | 100 +++++++ .../Sources/DIDCommAgent/DIDCommAgent.swift | 221 ++++++++++++++ .../Sources/EdgeAgent+Credentials.swift | 123 ++------ .../EdgeAgent+DIDHigherFucntions.swift | 114 ------- .../EdgeAgent/Sources/EdgeAgent.swift | 185 +----------- .../EdgeAgent/Sources/EdgeAgentErrors.swift | 61 ++++ .../Sources/OIDCAgent/OIDCAgent+DIDs.swift | 43 +++ .../Sources/OIDCAgent/OIDCAgent.swift | 198 ++++++++++++ .../Tests/AnoncredsPresentationFlowTest.swift | 11 +- .../EdgeAgent/Tests/BackupWalletTests.swift | 4 +- EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift | 2 +- .../EdgeAgent/Tests/Helper/MockPollux.swift | 24 ++ .../Tests/PresentationExchangeTests.swift | 11 +- ...dsCredentialStack+ProvableCredential.swift | 50 ++++ .../JWTCredential+ProofableCredential.swift | 61 +++- .../Sources/Models/JWT/JWTCredential.swift | 19 +- .../Sources/Models/JWT/JWTPresentation.swift | 21 +- .../SDJWT/SDJWT+ProvableCredential.swift | 42 ++- .../Models/SDJWT/SDJWTPresentation.swift | 21 +- .../PolluxImpl+CredentialRequest.swift | 31 ++ .../PolluxImpl+CredentialVerification.swift | 90 +++--- .../Sources/PolluxImpl+ParseCredential.swift | 59 ++++ Package.swift | 12 +- .../project.pbxproj | 6 + .../DeepLinkWebView.swift | 50 ++++ .../AtalaPrismWalletDemo/Info.plist | 17 +- .../DID/DIDFuncionalitiesViewModel.swift | 4 +- .../SetupPrismAgentViewModel.swift | 4 +- .../SigningVerificationViewModel.swift | 6 +- .../Verifier/Main/MainVerifierRouter.swift | 20 +- .../CreatePresentationViewModel.swift | 5 +- .../Presentations/PresentationsRouter.swift | 4 +- .../PresentationDetailViewModel.swift | 4 +- .../AddNewContact/AddNewContactBuilder.swift | 3 +- .../AddNewContact/AddNewContactView.swift | 31 ++ .../AddNewContactViewModel.swift | 51 +++- .../Backup/BackupViewBuilder.swift | 2 +- .../WalletDemo2/Backup/BackupViewModel.swift | 8 +- .../ConnectionsListViewModel.swift | 5 +- .../CredentialDetailViewModel.swift | 6 +- .../CredentialListRouter.swift | 2 +- .../CredentialListViewModel.swift | 12 +- .../DIDs/DIDList/DIDListViewModel.swift | 4 +- .../WalletDemo2/Main/Main2Router.swift | 16 +- .../MediatorPage/MediatorPageViewModel.swift | 4 +- .../MessageDetailViewModel.swift | 6 +- .../MessagesList/MessagesListRouter.swift | 2 +- .../MessagesList/MessagesListViewModel.swift | 6 +- .../Settings/SettingsViewRouter.swift | 4 +- 55 files changed, 1769 insertions(+), 622 deletions(-) create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift rename EdgeAgentSDK/EdgeAgent/Sources/{EdgeAgent+Invitations.swift => DIDCommAgent/DIDCommAgent+Invitations.swift} (98%) rename EdgeAgentSDK/EdgeAgent/Sources/{EdgeAgent+MessageEvents.swift => DIDCommAgent/DIDCommAgent+Messages.swift} (92%) create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Proof.swift create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent.swift create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift create mode 100644 EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent.swift create mode 100644 Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/DeepLinkWebView.swift diff --git a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift index 2567a194..0c4f5827 100644 --- a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift +++ b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift @@ -14,6 +14,8 @@ public enum CredentialOperationsOptions { case exportableKey(ExportableKey) // A key that can be exported. case zkpPresentationParams(attributes: [String: Bool], predicates: [String]) // Anoncreds zero-knowledge proof presentation parameters case disclosingClaims(claims: [String]) + case thid(String) + case presentationRequestId(String) case custom(key: String, data: Data) // Any custom data. } @@ -23,8 +25,18 @@ public protocol Pollux { /// - Parameter data: The encoded item to parse. /// - Throws: An error if the item cannot be parsed or decoded. /// - Returns: An object representing the parsed item. + @available(*, deprecated, message: "Please use the new method for parseCredential(type: String, credentialPayload: Data, options: [CredentialOperationsOptions])") func parseCredential(issuedCredential: Message, options: [CredentialOperationsOptions]) async throws -> Credential + /// Parses an encoded item and returns an object representing the parsed item. + /// - Parameters: + /// - type: The type of the credential, (`jwt`, `prism/jwt`, `vc+sd-jwt`, `anoncreds`, `anoncreds/credential@v1.0`) + /// - credentialPayload: The encoded credential to parse. + /// - options: Options required for some types of credentials. + /// - Throws: An error if the item cannot be parsed or decoded. + /// - Returns: An object representing the parsed item. + func parseCredential(type: String, credentialPayload: Data, options: [CredentialOperationsOptions]) async throws -> Credential + /// Restores a previously stored item using the provided restoration identifier and data. /// - Parameters: /// - restorationIdentifier: The identifier to use when restoring the item. @@ -39,11 +51,25 @@ public protocol Pollux { /// - options: The options to use when processing the request. /// - Throws: An error if the request cannot be processed. /// - Returns: A string representing the result of the request process. + @available(*, deprecated, message: "Please use the new method for processCredentialRequest(type: String, offerPayload: Data, options: [CredentialOperationsOptions])") func processCredentialRequest( offerMessage: Message, options: [CredentialOperationsOptions] ) async throws -> String + /// Processes a request based on a provided offer message and options. + /// - Parameters: + /// - type: The type of the credential, (`jwt`, `prism/jwt`, `vc+sd-jwt`, `anoncreds`, `anoncreds/credential-offer@v1.0`) + /// - offerMessage: The offer message that contains the details of the request. + /// - options: The options to use when processing the request. + /// - Throws: An error if the request cannot be processed. + /// - Returns: A string representing the result of the request process. + func processCredentialRequest( + type: String, + offerPayload: Data, + options: [CredentialOperationsOptions] + ) async throws -> String + /// Creates a presentation request for credentials of a specified type, directed to a specific DID, with additional metadata and filtering options. /// /// - Parameters: @@ -69,10 +95,25 @@ public protocol Pollux { /// - options: An array of options that influence how the presentation verification is conducted. /// - Returns: A Boolean value indicating whether the presentation is valid (`true`) or not (`false`). /// - Throws: An error if there is a problem verifying the presentation. + @available(*, deprecated, message: "Please use the new method for verifyPresentation(type: String, presentationPayload: Data, options: [CredentialOperationsOptions])") func verifyPresentation( message: Message, options: [CredentialOperationsOptions] ) async throws -> Bool + + /// Verifies the validity of a presentation contained within a message, using specified options. + /// + /// - Parameters: + /// - type: The type of the credential, (`jwt`, `prism/jwt`, `vc+sd-jwt`, `anoncreds`, `anoncreds/credential-presentation@v1.0`) + /// - presentationPayload: The message containing the presentation to be verified. + /// - options: An array of options that influence how the presentation verification is conducted. + /// - Returns: A Boolean value indicating whether the presentation is valid (`true`) or not (`false`). + /// - Throws: An error if there is a problem verifying the presentation. + func verifyPresentation( + type: String, + presentationPayload: Data, + options: [CredentialOperationsOptions] + ) async throws -> Bool } public extension Pollux { diff --git a/EdgeAgentSDK/Domain/Sources/Models/Credentials/ProvableCredential.swift b/EdgeAgentSDK/Domain/Sources/Models/Credentials/ProvableCredential.swift index 4826a5dc..db985a00 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/Credentials/ProvableCredential.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/Credentials/ProvableCredential.swift @@ -9,8 +9,18 @@ public protocol ProvableCredential { /// - options: The options to use when creating the proof. /// - Returns: The proof as a `String`. /// - Throws: If there is an error creating the proof. + @available(*, deprecated, message: "Please use the new method for presentation(type: requestPayload: options:)") func presentation(request: Message, options: [CredentialOperationsOptions]) throws -> String + /// Creates a presentation proof for a request message with the given options. + /// + /// - Parameters: + /// - request: The request message for which the proof needs to be created. + /// - options: The options to use when creating the proof. + /// - Returns: The proof as a `String`. + /// - Throws: If there is an error creating the proof. + func presentation(type: String, requestPayload: Data, options: [CredentialOperationsOptions]) throws -> String + /// Validates if the credential can be used for the given presentation request, using the specified options. /// /// - Parameters: @@ -18,7 +28,17 @@ public protocol ProvableCredential { /// - options: Options that may influence the validation process. /// - Returns: A Boolean indicating whether the credential is valid for the presentation (`true`) or not (`false`). /// - Throws: If there is an error during the validation process. + @available(*, deprecated, message: "Please use the new method for isValidForPresentation(type: requestPayload: options:)") func isValidForPresentation(request: Message, options: [CredentialOperationsOptions]) throws -> Bool + + /// Validates if the credential can be used for the given presentation request, using the specified options. + /// + /// - Parameters: + /// - request: The presentation request message to be validated against. + /// - options: Options that may influence the validation process. + /// - Returns: A Boolean indicating whether the credential is valid for the presentation (`true`) or not (`false`). + /// - Throws: If there is an error during the validation process. + func isValidForPresentation(type: String, requestPayload: Data, options: [CredentialOperationsOptions]) throws -> Bool } public extension Credential { diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift new file mode 100644 index 00000000..baebb148 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift @@ -0,0 +1,282 @@ +import Core +import Combine +import Domain +import Foundation +import Logging +import JSONWebToken + +public extension DIDCommAgent { + + /// This function initiates a presentation request for a specific type of credential, specifying the sender's and receiver's DIDs, and any claim filters applicable. + /// + /// - Parameters: + /// - type: The type of the credential for which the presentation is requested. + /// - fromDID: The decentralized identifier (DID) of the entity initiating the request. + /// - toDID: The decentralized identifier (DID) of the entity to which the request is being sent. + /// - claimFilters: A collection of filters specifying the claims required in the credential. + /// - Returns: The initiated request for presentation. + /// - Throws: EdgeAgentError, if there is a problem initiating the presentation request. + func initiatePresentationRequest( + type: CredentialType, + fromDID: DID, + toDID: DID, + claimFilters: [ClaimFilter] + ) async throws -> RequestPresentation { + let rqstStr = try await edgeAgent.initiatePresentationRequest( + type: type, + fromDID: fromDID, + toDID: toDID, + claimFilters: claimFilters + ) + let attachment: AttachmentDescriptor + switch type { + case .jwt: + let data = try AttachmentBase64(base64: rqstStr.tryToData().base64URLEncoded()) + attachment = AttachmentDescriptor( + mediaType: "application/json", + data: data, + format: "dif/presentation-exchange/definitions@v1.0" + ) + case .anoncred: + let data = try AttachmentBase64(base64: rqstStr.tryToData().base64URLEncoded()) + attachment = AttachmentDescriptor( + mediaType: "application/json", + data: data, + format: "anoncreds/proof-request@v1.0" + ) + } + + return RequestPresentation( + body: .init( + proofTypes: [ProofTypes( + schema: "", + requiredFields: claimFilters.flatMap(\.paths), + trustIssuers: nil + )] + ), + attachments: [attachment], + thid: nil, + from: fromDID, + to: toDID + ) + } + + /// This function verifies the presentation contained within a message. + /// + /// - Parameters: + /// - message: The message containing the presentation to be verified. + /// - Returns: A Boolean value indicating whether the presentation is valid (`true`) or not (`false`). + /// - Throws: EdgeAgentError, if there is a problem verifying the presentation. + + func verifyPresentation(message: Message) async throws -> Bool { + do { + let downloader = DownloadDataWithResolver(castor: castor) + guard + let attachment = message.attachments.first, + let requestId = message.thid + else { + throw PolluxError.couldNotFindPresentationInAttachments + } + + let jsonData: Data + switch attachment.data { + case let attchedData as AttachmentBase64: + guard let decoded = Data(fromBase64URL: attchedData.base64) else { + throw CommonError.invalidCoding(message: "Invalid base64 url attachment") + } + jsonData = decoded + case let attchedData as AttachmentJsonData: + jsonData = attchedData.data + default: + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + + guard let format = attachment.format else { + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + + return try await pollux.verifyPresentation( + type: format, + presentationPayload: jsonData, + options: [ + .presentationRequestId(requestId), + .credentialDefinitionDownloader(downloader: downloader), + .schemaDownloader(downloader: downloader) + ]) + } catch { + logger.error(error: error) + throw error + } + } + + /// This function parses an issued credential message, stores and returns the verifiable credential. + /// + /// - Parameters: + /// - message: Issue credential Message. + /// - Returns: The parsed verifiable credential. + /// - Throws: EdgeAgentError, if there is a problem parsing the credential. + func processIssuedCredentialMessage(message: IssueCredential3_0) async throws -> Credential { + guard + let linkSecret = try await pluto.getLinkSecret().first().await() + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let restored = try await self.apollo.restoreKey(linkSecret) + guard + let linkSecretString = String(data: restored.raw, encoding: .utf8) + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let downloader = DownloadDataWithResolver(castor: castor) + guard + let attachment = message.attachments.first, + let format = attachment.format + else { + throw PolluxError.unsupportedIssuedMessage + } + + let jsonData: Data + switch attachment.data { + case let attchedData as AttachmentBase64: + guard let decoded = Data(fromBase64URL: attchedData.base64) else { + throw CommonError.invalidCoding(message: "Invalid base64 url attachment") + } + jsonData = decoded + case let attchedData as AttachmentJsonData: + jsonData = attchedData.data + default: + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + + let credential = try await pollux.parseCredential( + type: format, + credentialPayload: jsonData, + options: [ + .linkSecret(id: "", secret: linkSecretString), + .credentialDefinitionDownloader(downloader: downloader), + .schemaDownloader(downloader: downloader) + ] + ) + + guard let storableCredential = credential.storable else { + return credential + } + try await pluto + .storeCredential(credential: storableCredential) + .first() + .await() + return credential + } + + /// This function prepares a request credential from an offer given the subject DID. + /// + /// - Parameters: + /// - did: Subject DID. + /// - did: Received offer credential. + /// - Returns: Created request credential + /// - Throws: EdgeAgentError, if there is a problem creating the request credential. + func prepareRequestCredentialWithIssuer(did: DID, offer: OfferCredential3_0) async throws -> RequestCredential3_0? { + guard did.method == "prism" else { throw PolluxError.invalidPrismDID } + let didInfo = try await pluto + .getDIDInfo(did: did) + .first() + .await() + + guard let storedPrivateKey = didInfo?.privateKeys.first else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let privateKey = try await apollo.restorePrivateKey(storedPrivateKey) + + guard + let exporting = privateKey.exporting, + let linkSecret = try await pluto.getLinkSecret().first().await() + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let restored = try await self.apollo.restoreKey(linkSecret) + guard + let linkSecretString = String(data: restored.raw, encoding: .utf8) + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let downloader = DownloadDataWithResolver(castor: castor) + guard + let attachment = offer.attachments.first, + let offerFormat = attachment.format + else { + throw PolluxError.unsupportedIssuedMessage + } + + let jsonData: Data + switch attachment.data { + case let attchedData as AttachmentBase64: + guard let decoded = Data(fromBase64URL: attchedData.base64) else { + throw CommonError.invalidCoding(message: "Invalid base64 url attachment") + } + jsonData = decoded + case let attchedData as AttachmentJsonData: + jsonData = attchedData.data + default: + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + let requestString = try await pollux.processCredentialRequest( + type: offerFormat, + offerPayload: jsonData, + options: [ + .exportableKey(exporting), + .subjectDID(did), + .linkSecret(id: did.string, secret: linkSecretString), + .credentialDefinitionDownloader(downloader: downloader), + .schemaDownloader(downloader: downloader) + ] + ) + + guard + let base64String = requestString.data(using: .utf8)?.base64EncodedString() + else { + throw CommonError.invalidCoding(message: "Could not encode to base64") + } + guard + let offerPiuri = ProtocolTypes(rawValue: offer.type) + else { + throw EdgeAgentError.invalidMessageType( + type: offer.type, + shouldBe: [ + ProtocolTypes.didcommOfferCredential3_0.rawValue + ] + ) + } + let format: String + switch offerFormat { + case "prism/jwt": + format = "prism/jwt" + case "vc+sd-jwt": + format = "vc+sd-jwt" + case "anoncreds/credential-offer@v1.0": + format = "anoncreds/credential-request@v1.0" + default: + throw EdgeAgentError.invalidMessageType( + type: offerFormat, + shouldBe: [ + "prism/jwt", + "anoncreds/credential-offer@v1.0" + ] + ) + } + + let type = offerPiuri == .didcommOfferCredential ? + ProtocolTypes.didcommRequestCredential : + ProtocolTypes.didcommRequestCredential3_0 + + let requestCredential = RequestCredential3_0( + body: .init( + goalCode: offer.body.goalCode, + comment: offer.body.comment + ), + type: type.rawValue, + attachments: [.init( + data: AttachmentBase64(base64: base64String), + format: format + )], + thid: offer.thid, + from: offer.to, + to: offer.from + ) + return requestCredential + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift new file mode 100644 index 00000000..fb0a8a0e --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift @@ -0,0 +1,165 @@ +import Combine +import Domain +import Foundation + +// MARK: DID High Level functionalities +public extension DIDCommAgent { + + /// This method create a new Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - services: an array of services associated to the DID + /// - Returns: The new created DID + func createNewPrismDID( + keyPathIndex: Int? = nil, + alias: String? = nil, + services: [DIDDocument.Service] = [] + ) async throws -> DID { + try await edgeAgent.createNewPrismDID( + keyPathIndex: keyPathIndex, + alias: alias, + services: services + ) + } + + /// This method registers a Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - did: the DID which will be registered. + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - Returns: The new created DID + func registerPrismDID( + did: DID, + privateKey: PrivateKey, + alias: String? = nil + ) async throws { + try await edgeAgent.registerPrismDID( + did: did, + privateKey: privateKey, + alias: alias + ) + } + /// This function creates a new Peer DID, stores it in pluto database and updates the mediator if requested. + /// + /// - Parameters: + /// - services: The services associated to the new DID. + /// - updateMediator: Indicates if the new DID should be added to the mediator's list. It will as well add the mediator service. + /// - Returns: A new DID + /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available or if storing the new DID failed + func createNewPeerDID( + services: [DIDDocument.Service] = [], + alias: String? = "", + updateMediator: Bool + ) async throws -> DID { + var keyAgreementPrivateKey = try apollo.createPrivateKey(parameters: [ + KeyProperties.type.rawValue: "EC", + KeyProperties.curve.rawValue: KnownKeyCurves.x25519.rawValue + ]) + + var authenticationPrivateKey = try apollo.createPrivateKey(parameters: [ + KeyProperties.type.rawValue: "EC", + KeyProperties.curve.rawValue: KnownKeyCurves.ed25519.rawValue + ]) + + let withServices: [DIDDocument.Service] + if updateMediator, let routingDID = mediatorRoutingDID?.string { + withServices = services + [.init( + id: "#didcomm-1", + type: ["DIDCommMessaging"], + serviceEndpoint: [.init( + uri: routingDID + )])] + } else { + withServices = services + } + + let newDID = try castor.createPeerDID( + keyAgreementPublicKey: keyAgreementPrivateKey.publicKey(), + authenticationPublicKey: authenticationPrivateKey.publicKey(), + services: withServices + ) + + let didDocument = try await castor.resolveDID(did: newDID) + didDocument.authenticate.first.map { authenticationPrivateKey.identifier = $0.id.string } + didDocument.keyAgreement.first.map { keyAgreementPrivateKey.identifier = $0.id.string } + + logger.debug(message: "Created new Peer DID", metadata: [ + .maskedMetadataByLevel(key: "DID", value: newDID.string, level: .debug) + ]) + + try await registerPeerDID( + did: newDID, + privateKeys: [ + keyAgreementPrivateKey, + authenticationPrivateKey + ], + alias: alias, + updateMediator: updateMediator + ) + + return newDID + } + + /// This function registers a Peer DID, stores it and his private key in pluto database and updates the mediator if requested. + /// + /// - Parameters: + /// - services: The services associated to the new DID. + /// - updateMediator: Indicates if the new DID should be added to the mediator's list. + /// - Returns: A new DID + /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available or if storing the new DID failed + func registerPeerDID( + did: DID, + privateKeys: [PrivateKey], + alias: String?, + updateMediator: Bool + ) async throws { + if updateMediator { + try await updateMediatorWithDID(did: did) + } + logger.debug(message: "Register of DID in storage", metadata: [ + .maskedMetadataByLevel(key: "DID", value: did.string, level: .debug) + ]) + + let storablePrivateKeys = try privateKeys + .map { + guard let storablePrivateKey = $0 as? (PrivateKey & StorableKey) else { + throw KeyError.keyRequiresConformation(conformations: ["PrivateKey", "StorableKey"]) + } + return storablePrivateKey + } + + try await pluto + .storePeerDID( + did: did, + privateKeys: storablePrivateKeys, + alias: alias + ) + .first() + .await() + } + + /// This function updates the mediator key list with a new DID. + /// + /// - Parameters: + /// - services: The services associated to the new DID. + /// - updateMediator: Indicates if the new DID should be added to the mediator's list. + /// - Returns: A new DID + /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available + func updateMediatorWithDID(did: DID) async throws { + logger.debug(message: "Update mediator key list with DID", metadata: [ + .maskedMetadataByLevel(key: "DID", value: did.string, level: .debug) + ]) + + try await mediationHandler?.updateKeyListWithDIDs(dids: [did]) + } + + /// This function gets all the DID peers from the `pluto` store + /// + /// - Returns: A publisher that emits an array of tuples (`DID`, `String?`) objects, or an error if there was a problem getting the dids + func getAllRegisteredPeerDIDs() -> AnyPublisher<[(did: DID, alias: String?)], Error> { + pluto.getAllPeerDIDs() + .map { $0.map { ($0.did, $0.alias) } } + .eraseToAnyPublisher() + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Invitations.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift similarity index 98% rename from EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Invitations.swift rename to EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift index ff2aefb0..b263a050 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Invitations.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Invitations.swift @@ -1,8 +1,7 @@ import Domain import Foundation -// MARK: Invitation funcionalities -public extension EdgeAgent { +public extension DIDCommAgent { /// Enumeration representing the type of invitation enum InvitationType { /// Struct representing a Prism Onboarding invitation @@ -21,6 +20,50 @@ public extension EdgeAgent { case onboardingDIDComm(OutOfBandInvitation) } + /// Parses the given string as an Out-of-Band invitation + /// - Parameter url: The string to parse + /// - Returns: The parsed Out-of-Band invitation + /// - Throws: `EdgeAgentError` if the string is not a valid URL + func parseOOBInvitation(url: String) throws -> OutOfBandInvitation { + guard let url = URL(string: url) else { throw CommonError.invalidURLError(url: url) } + return try parseOOBInvitation(url: url) + } + + /// Parses the given URL as an Out-of-Band invitation + /// - Parameter url: The URL to parse + /// - Returns: The parsed Out-of-Band invitation + /// - Throws: `EdgeAgentError` if the URL is not a valid Out-of-Band invitation + func parseOOBInvitation(url: URL) throws -> OutOfBandInvitation { + return try DIDCommInvitationRunner(url: url).run() + } + + /// Accepts a Prism Onboarding invitation and performs the onboarding process + /// - Parameter invitation: The Prism Onboarding invitation to accept + /// - Throws: `EdgeAgentError` if the onboarding process fails + func acceptPrismInvitation(invitation: InvitationType.PrismOnboarding) async throws { + struct SendDID: Encodable { + let did: String + } + var request = URLRequest(url: invitation.endpoint) + request.httpMethod = "POST" + request.httpBody = try JSONEncoder().encode(SendDID(did: invitation.ownDID.string)) + request.setValue("application/json", forHTTPHeaderField: "content-type") + do { + let response = try await URLSession.shared.data(for: request) + guard let urlResponse = response.1 as? HTTPURLResponse else { + throw CommonError.invalidCoding( + message: "This should not happen cannot convert URLResponse to HTTPURLResponse" + ) + } + guard urlResponse.statusCode == 200 else { + throw CommonError.httpError( + code: urlResponse.statusCode, + message: String(data: response.0, encoding: .utf8) ?? "" + ) + } + } + } + /// Parses the given string as an invitation /// - Parameter str: The string to parse /// - Returns: The parsed invitation @@ -64,23 +107,6 @@ public extension EdgeAgent { ) } - /// Parses the given string as an Out-of-Band invitation - /// - Parameter url: The string to parse - /// - Returns: The parsed Out-of-Band invitation - /// - Throws: `EdgeAgentError` if the string is not a valid URL - func parseOOBInvitation(url: String) throws -> OutOfBandInvitation { - guard let url = URL(string: url) else { throw CommonError.invalidURLError(url: url) } - return try parseOOBInvitation(url: url) - } - - /// Parses the given URL as an Out-of-Band invitation - /// - Parameter url: The URL to parse - /// - Returns: The parsed Out-of-Band invitation - /// - Throws: `EdgeAgentError` if the URL is not a valid Out-of-Band invitation - func parseOOBInvitation(url: URL) throws -> OutOfBandInvitation { - return try DIDCommInvitationRunner(url: url).run() - } - /// Accepts an Out-of-Band (DIDComm) invitation and establishes a new connection /// - Parameter invitation: The Out-of-Band invitation to accept /// - Throws: `EdgeAgentError` if there is no mediator available or other errors occur during the acceptance process @@ -101,31 +127,4 @@ public extension EdgeAgent { ).run() try await connectionManager.addConnection(pair) } - - /// Accepts a Prism Onboarding invitation and performs the onboarding process - /// - Parameter invitation: The Prism Onboarding invitation to accept - /// - Throws: `EdgeAgentError` if the onboarding process fails - func acceptPrismInvitation(invitation: InvitationType.PrismOnboarding) async throws { - struct SendDID: Encodable { - let did: String - } - var request = URLRequest(url: invitation.endpoint) - request.httpMethod = "POST" - request.httpBody = try JSONEncoder().encode(SendDID(did: invitation.ownDID.string)) - request.setValue("application/json", forHTTPHeaderField: "content-type") - do { - let response = try await URLSession.shared.data(for: request) - guard let urlResponse = response.1 as? HTTPURLResponse else { - throw CommonError.invalidCoding( - message: "This should not happen cannot convert URLResponse to HTTPURLResponse" - ) - } - guard urlResponse.statusCode == 200 else { - throw CommonError.httpError( - code: urlResponse.statusCode, - message: String(data: response.0, encoding: .utf8) ?? "" - ) - } - } - } } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+MessageEvents.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Messages.swift similarity index 92% rename from EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+MessageEvents.swift rename to EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Messages.swift index c4f6834a..f5651e4e 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+MessageEvents.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Messages.swift @@ -2,8 +2,7 @@ import Combine import Domain import Foundation -// MARK: Messaging events funcionalities -public extension EdgeAgent { +public extension DIDCommAgent { /// Start fetching the messages from the mediator func startFetchingMessages(timeBetweenRequests: Int = 5) { let timeInterval = max(timeBetweenRequests, 5) @@ -26,10 +25,6 @@ public extension EdgeAgent { logger.error(error: error) } sleep(UInt32(timeInterval)) - - if (messagesStreamTask?.isCancelled == true) { - break - } } } } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Proof.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Proof.swift new file mode 100644 index 00000000..8a7d1b3a --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Proof.swift @@ -0,0 +1,100 @@ +import Core +import Combine +import Domain +import Foundation +import Logging +import JSONWebToken + +public extension DIDCommAgent { + + /// This function creates a Presentation from a request verfication. + /// + /// - Parameters: + /// - request: Request message received. + /// - credential: Verifiable Credential to present. + /// - Returns: Presentation message prepared to send. + /// - Throws: EdgeAgentError, if there is a problem creating the presentation. + func createPresentationForRequestProof( + request: RequestPresentation, + credential: Credential, + options: [CredentialOperationsOptions] = [] + ) async throws -> Presentation { + guard let proofableCredential = credential.proof else { + throw EdgeAgentError.credentialCannotIssuePresentations + } + + guard let requestType = request.attachments.first?.format else { + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + let presentationString: String + let format: String + switch requestType { + case "anoncreds/proof-request@v1.0": + guard + let linkSecret = try await pluto.getLinkSecret().first().await() + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let restored = try await self.apollo.restoreKey(linkSecret) + guard + let linkSecretString = String(data: restored.raw, encoding: .utf8) + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + format = "anoncreds/proof@v1.0" + presentationString = try proofableCredential.presentation( + request: request.makeMessage(), + options: options + [ + .linkSecret(id: "", secret: linkSecretString) + ] + ) + case "prism/jwt", "vc+sd-jwt", "dif/presentation-exchange/definitions@v1.0": + guard + let subjectDIDString = credential.subject + else { + throw PolluxError.invalidPrismDID + } + + let subjectDID = try DID(string: subjectDIDString) + + let privateKeys = try await pluto.getDIDPrivateKeys(did: subjectDID).first().await() + + guard + let storedPrivateKey = privateKeys?.first + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + let privateKey = try await apollo.restorePrivateKey(storedPrivateKey) + + guard + let exporting = privateKey.exporting + else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + + format = requestType == "prism/jwt" ? "prism/jwt" : "dif/presentation-exchange/submission@v1.0" + + presentationString = try proofableCredential.presentation( + request: request.makeMessage(), + options: options + [ + .exportableKey(exporting), + .subjectDID(subjectDID) + ] + ) + default: + throw EdgeAgentError.invalidAttachmentFormat(requestType) + } + + Logger(label: "").log(level: .info, "Presentation: \(presentationString)") + + let base64String = try presentationString.tryToData().base64URLEncoded() + + return Presentation( + body: .init( + goalCode: request.body.goalCode, + comment: request.body.comment + ), + attachments: [.init( + data: AttachmentBase64(base64: base64String), + format: format + )], + thid: request.thid ?? request.id, + from: request.to, + to: request.from + ) + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent.swift new file mode 100644 index 00000000..9b38b182 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent.swift @@ -0,0 +1,221 @@ +import Builders +import Combine +import Core +import Domain +import Foundation + +public class DIDCommAgent { + /// Enumeration representing the current state of the agent. + public enum State: String { + case stoped + case starting + case running + case stoping + } + + /// Represents the current state of the agent. + public private(set) var state = State.stoped + + /// The mediator routing DID if one is currently registered. + public var mediatorRoutingDID: DID? { + connectionManager?.mediationHandler.mediator?.routingDID + } + + public let mercury: Mercury + public let edgeAgent: EdgeAgent + public var apollo: Apollo & KeyRestoration { edgeAgent.apollo } + public var castor: Castor { edgeAgent.castor } + public var pluto: Pluto { edgeAgent.pluto } + public var pollux: Pollux { edgeAgent.pollux } + var logger: SDKLogger { edgeAgent.logger } + + var mediationHandler: MediatorHandler? + + var connectionManager: ConnectionsManagerImpl? + var cancellables = [AnyCancellable]() + // Not a "stream" + var messagesStreamTask: Task? + + /// Initializes a EdgeAgent with the given dependency objects and seed data. + /// + /// - Parameters: + /// - apollo: An instance of Apollo. + /// - castor: An instance of Castor. + /// - pluto: An instance of Pluto. + /// - pollux: An instance of Pollux. + /// - mercury: An instance of Mercury. + /// - seed: A unique seed used to generate the unique DID. + /// - mediatorServiceEnpoint: The endpoint of the Mediator service to use. + public init( + edgeAgent: EdgeAgent, + mercury: Mercury, + mediationHandler: MediatorHandler? = nil + ) { + self.edgeAgent = edgeAgent + self.mercury = mercury + self.mediationHandler = mediationHandler + mediationHandler.map { + self.connectionManager = ConnectionsManagerImpl( + castor: edgeAgent.castor, + mercury: mercury, + pluto: edgeAgent.pluto, + mediationHandler: $0, + pairings: [] + ) + } + } + + /** + Convenience initializer for `EdgeAgent` that allows for optional initialization of seed data and mediator service endpoint. + + - Parameters: + - seedData: Optional seed data for creating a new seed. If not provided, a random seed will be generated. + - mediatorServiceEnpoint: Optional DID representing the service endpoint of the mediator. If not provided, the default Prism mediator endpoint will be used. + */ + public convenience init( + seedData: Data? = nil, + mediatorDID: DID + ) { + let edgeAgent = EdgeAgent(seedData: seedData) + let secretsStream = createSecretsStream( + keyRestoration: edgeAgent.apollo, + pluto: edgeAgent.pluto, + castor: edgeAgent.castor + ) + + let mercury = MercuryBuilder( + castor: edgeAgent.castor, + secretsStream: secretsStream + ).build() + + self.init( + edgeAgent: edgeAgent, + mercury: mercury, + mediationHandler: BasicMediatorHandler( + mediatorDID: mediatorDID, + mercury: mercury, + store: BasicMediatorHandler.PlutoMediatorStoreImpl(pluto: edgeAgent.pluto) + ) + ) + } + + public func setupMediatorHandler(mediationHandler: MediatorHandler) async throws { + try await stop() + self.mediationHandler = mediationHandler + self.connectionManager = ConnectionsManagerImpl( + castor: castor, + mercury: mercury, + pluto: pluto, + mediationHandler: mediationHandler, + pairings: [] + ) + } + + public func setupMediatorDID(did: DID) async throws { + let mediatorHandler = BasicMediatorHandler( + mediatorDID: did, + mercury: mercury, + store: BasicMediatorHandler.PlutoMediatorStoreImpl(pluto: pluto) + ) + try await setupMediatorHandler(mediationHandler: mediatorHandler) + } + + /** + Start the EdgeAgent and Mediator services + + - Throws: EdgeAgentError.noMediatorAvailableError if no mediator is available, + as well as any error thrown by `createNewPeerDID` and `connectionManager.registerMediator` + */ + public func start() async throws { + guard + let connectionManager, + state == .stoped + else { return } + logger.info(message: "Starting agent") + state = .starting + do { + try await connectionManager.startMediator() + } catch EdgeAgentError.noMediatorAvailableError { + let hostDID = try await createNewPeerDID(updateMediator: false) + try await connectionManager.registerMediator(hostDID: hostDID) + } + try await edgeAgent.firstLinkSecretSetup() + state = .running + logger.info(message: "Mediation Achieved", metadata: [ + .publicMetadata(key: "Routing DID", value: mediatorRoutingDID?.string ?? "") + ]) + logger.info(message: "Agent running") + } + + /** + This function is used to stop the EdgeAgent. + The function sets the state of EdgeAgent to .stoping. + All ongoing events that was created by the EdgeAgent are stopped. + After all the events are stopped the state of the EdgeAgent is set to .stoped. + + - Throws: If the current state is not running throws error. + */ + public func stop() async throws { + guard state == .running else { return } + logger.info(message: "Stoping agent") + state = .stoping + cancellables.forEach { $0.cancel() } + connectionManager?.stopAllEvents() + state = .stoped + logger.info(message: "Agent not running") + } +} + +private func createSecretsStream( + keyRestoration: KeyRestoration, + pluto: Pluto, + castor: Castor +) -> AnyPublisher<[Secret], Error> { + pluto.getAllKeys() + .first() + .flatMap { keys in + Future { + let privateKeys = await keys.asyncMap { + try? await keyRestoration.restorePrivateKey($0) + }.compactMap { $0 } + return try parsePrivateKeys( + privateKeys: privateKeys, + castor: castor + ) + } + } + .eraseToAnyPublisher() +} + +private func parsePrivateKeys( + privateKeys: [PrivateKey], + castor: Castor +) throws -> [Domain.Secret] { + return try privateKeys + .map { $0 as? (PrivateKey & ExportableKey & StorableKey) } + .compactMap { $0 } + .map { privateKey in + return privateKey + } + .map { privateKey in + try parseToSecret( + privateKey: privateKey, + identifier: privateKey.identifier + ) + } +} + +private func parseToSecret(privateKey: PrivateKey & ExportableKey, identifier: String) throws -> Domain.Secret { + let jwk = privateKey.jwk + guard + let dataJson = try? JSONEncoder().encode(jwk), + let stringJson = String(data: dataJson, encoding: .utf8) + else { + throw CommonError.invalidCoding(message: "Could not encode privateKey.jwk") + } + return .init( + id: identifier, + type: .jsonWebKey2020, + secretMaterial: .jwk(value: stringJson) + ) +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift index 5ecd2e1d..d6e88e79 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift @@ -38,7 +38,7 @@ public extension EdgeAgent { fromDID: DID, toDID: DID, claimFilters: [ClaimFilter] - ) async throws -> RequestPresentation { + ) async throws -> String { let request = try self.pollux.createPresentationRequest( type: type, toDID: toDID, @@ -49,37 +49,7 @@ public extension EdgeAgent { let rqstStr = try request.tryToString() logger.debug(message: "Request: \(rqstStr)") - let attachment: AttachmentDescriptor - switch type { - case .jwt: - let data = AttachmentBase64(base64: request.base64URLEncoded()) - attachment = AttachmentDescriptor( - mediaType: "application/json", - data: data, - format: "dif/presentation-exchange/definitions@v1.0" - ) - case .anoncred: - let data = AttachmentBase64(base64: request.base64URLEncoded()) - attachment = AttachmentDescriptor( - mediaType: "application/json", - data: data, - format: "anoncreds/proof-request@v1.0" - ) - } - - return RequestPresentation( - body: .init( - proofTypes: [ProofTypes( - schema: "", - requiredFields: claimFilters.flatMap(\.paths), - trustIssuers: nil - )] - ), - attachments: [attachment], - thid: nil, - from: fromDID, - to: toDID - ) + return rqstStr } /// This function verifies the presentation contained within a message. @@ -89,13 +59,22 @@ public extension EdgeAgent { /// - Returns: A Boolean value indicating whether the presentation is valid (`true`) or not (`false`). /// - Throws: EdgeAgentError, if there is a problem verifying the presentation. - func verifyPresentation(message: Message) async throws -> Bool { + func verifyPresentation( + type: String, + presentationPayload: Data, + requestId: String + ) async throws -> Bool { do { let downloader = DownloadDataWithResolver(castor: castor) - return try await pollux.verifyPresentation(message: message, options: [ - .credentialDefinitionDownloader(downloader: downloader), - .schemaDownloader(downloader: downloader) - ]) + return try await pollux.verifyPresentation( + type: type, + presentationPayload: presentationPayload, + options: [ + .presentationRequestId(requestId), + .credentialDefinitionDownloader(downloader: downloader), + .schemaDownloader(downloader: downloader) + ] + ) } catch { logger.error(error: error) throw error @@ -108,7 +87,7 @@ public extension EdgeAgent { /// - message: Issue credential Message. /// - Returns: The parsed verifiable credential. /// - Throws: EdgeAgentError, if there is a problem parsing the credential. - func processIssuedCredentialMessage(message: IssueCredential3_0) async throws -> Credential { + func processIssuedCredential(type: String, issuedCredentialPayload: Data) async throws -> Credential { guard let linkSecret = try await pluto.getLinkSecret().first().await() else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } @@ -120,7 +99,8 @@ public extension EdgeAgent { let downloader = DownloadDataWithResolver(castor: castor) let credential = try await pollux.parseCredential( - issuedCredential: message.makeMessage(), + type: type, + credentialPayload: issuedCredentialPayload, options: [ .linkSecret(id: "", secret: linkSecretString), .credentialDefinitionDownloader(downloader: downloader), @@ -145,7 +125,11 @@ public extension EdgeAgent { /// - did: Received offer credential. /// - Returns: Created request credential /// - Throws: EdgeAgentError, if there is a problem creating the request credential. - func prepareRequestCredentialWithIssuer(did: DID, offer: OfferCredential3_0) async throws -> RequestCredential3_0? { + func prepareRequestCredentialWithIssuer( + did: DID, + type: String, + offerPayload: Data + ) async throws -> String { guard did.method == "prism" else { throw PolluxError.invalidPrismDID } let didInfo = try await pluto .getDIDInfo(did: did) @@ -167,8 +151,9 @@ public extension EdgeAgent { else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } let downloader = DownloadDataWithResolver(castor: castor) - let requestString = try await pollux.processCredentialRequest( - offerMessage: offer.makeMessage(), + return try await pollux.processCredentialRequest( + type: type, + offerPayload: offerPayload, options: [ .exportableKey(exporting), .subjectDID(did), @@ -177,59 +162,5 @@ public extension EdgeAgent { .schemaDownloader(downloader: downloader) ] ) - - guard - let offerFormat = offer.attachments.first?.format, - let base64String = requestString.data(using: .utf8)?.base64EncodedString() - else { - throw CommonError.invalidCoding(message: "Could not encode to base64") - } - guard - let offerPiuri = ProtocolTypes(rawValue: offer.type) - else { - throw EdgeAgentError.invalidMessageType( - type: offer.type, - shouldBe: [ - ProtocolTypes.didcommOfferCredential3_0.rawValue - ] - ) - } - let format: String - switch offerFormat { - case "prism/jwt": - format = "prism/jwt" - case "vc+sd-jwt": - format = "vc+sd-jwt" - case "anoncreds/credential-offer@v1.0": - format = "anoncreds/credential-request@v1.0" - default: - throw EdgeAgentError.invalidMessageType( - type: offerFormat, - shouldBe: [ - "prism/jwt", - "anoncreds/credential-offer@v1.0" - ] - ) - } - - let type = offerPiuri == .didcommOfferCredential ? - ProtocolTypes.didcommRequestCredential : - ProtocolTypes.didcommRequestCredential3_0 - - let requestCredential = RequestCredential3_0( - body: .init( - goalCode: offer.body.goalCode, - comment: offer.body.comment - ), - type: type.rawValue, - attachments: [.init( - data: AttachmentBase64(base64: base64String), - format: format - )], - thid: offer.thid, - from: offer.to, - to: offer.from - ) - return requestCredential } } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift index 45a65724..7c785270 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift @@ -132,120 +132,6 @@ Could not find key in storage please use Castor instead and provide the private .await() } - /// This function creates a new Peer DID, stores it in pluto database and updates the mediator if requested. - /// - /// - Parameters: - /// - services: The services associated to the new DID. - /// - updateMediator: Indicates if the new DID should be added to the mediator's list. It will as well add the mediator service. - /// - Returns: A new DID - /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available or if storing the new DID failed - func createNewPeerDID( - services: [DIDDocument.Service] = [], - alias: String? = "", - updateMediator: Bool - ) async throws -> DID { - var keyAgreementPrivateKey = try apollo.createPrivateKey(parameters: [ - KeyProperties.type.rawValue: "EC", - KeyProperties.curve.rawValue: KnownKeyCurves.x25519.rawValue - ]) - - var authenticationPrivateKey = try apollo.createPrivateKey(parameters: [ - KeyProperties.type.rawValue: "EC", - KeyProperties.curve.rawValue: KnownKeyCurves.ed25519.rawValue - ]) - - let withServices: [DIDDocument.Service] - if updateMediator, let routingDID = mediatorRoutingDID?.string { - withServices = services + [.init( - id: "#didcomm-1", - type: ["DIDCommMessaging"], - serviceEndpoint: [.init( - uri: routingDID - )])] - } else { - withServices = services - } - - let newDID = try castor.createPeerDID( - keyAgreementPublicKey: keyAgreementPrivateKey.publicKey(), - authenticationPublicKey: authenticationPrivateKey.publicKey(), - services: withServices - ) - - let didDocument = try await castor.resolveDID(did: newDID) - didDocument.authenticate.first.map { authenticationPrivateKey.identifier = $0.id.string } - didDocument.keyAgreement.first.map { keyAgreementPrivateKey.identifier = $0.id.string } - - logger.debug(message: "Created new Peer DID", metadata: [ - .maskedMetadataByLevel(key: "DID", value: newDID.string, level: .debug) - ]) - - try await registerPeerDID( - did: newDID, - privateKeys: [ - keyAgreementPrivateKey, - authenticationPrivateKey - ], - alias: alias, - updateMediator: updateMediator - ) - - return newDID - } - - /// This function registers a Peer DID, stores it and his private key in pluto database and updates the mediator if requested. - /// - /// - Parameters: - /// - services: The services associated to the new DID. - /// - updateMediator: Indicates if the new DID should be added to the mediator's list. - /// - Returns: A new DID - /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available or if storing the new DID failed - func registerPeerDID( - did: DID, - privateKeys: [PrivateKey], - alias: String?, - updateMediator: Bool - ) async throws { - if updateMediator { - try await updateMediatorWithDID(did: did) - } - logger.debug(message: "Register of DID in storage", metadata: [ - .maskedMetadataByLevel(key: "DID", value: did.string, level: .debug) - ]) - - let storablePrivateKeys = try privateKeys - .map { - guard let storablePrivateKey = $0 as? (PrivateKey & StorableKey) else { - throw KeyError.keyRequiresConformation(conformations: ["PrivateKey", "StorableKey"]) - } - return storablePrivateKey - } - - try await pluto - .storePeerDID( - did: did, - privateKeys: storablePrivateKeys, - alias: alias - ) - .first() - .await() - } - - /// This function updates the mediator key list with a new DID. - /// - /// - Parameters: - /// - services: The services associated to the new DID. - /// - updateMediator: Indicates if the new DID should be added to the mediator's list. - /// - Returns: A new DID - /// - Throws: EdgeAgentError, if updateMediator is true and there is no mediator available - func updateMediatorWithDID(did: DID) async throws { - logger.debug(message: "Update mediator key list with DID", metadata: [ - .maskedMetadataByLevel(key: "DID", value: did.string, level: .debug) - ]) - - try await mediationHandler?.updateKeyListWithDIDs(dids: [did]) - } - /// This function gets the DID information (alias) for a given DID /// /// - Parameter did: The DID for which the information is requested diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift index a2f28f6d..4acaf83f 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift @@ -7,42 +7,14 @@ import Foundation /// EdgeAgent class is responsible for handling the connection to other agents in the network using /// a provided Mediator Service Endpoint and seed data. public class EdgeAgent { - /// Enumeration representing the current state of the agent. - public enum State: String { - case stoped - case starting - case running - case stoping - } - /// Represents the seed data used to create a unique DID. public let seed: Seed - /// Represents the current state of the agent. - public private(set) var state = State.stoped - - // TODO: This is to be deleted - public private(set) var requestedPresentations: CurrentValueSubject< - [(RequestPresentation, Bool)], Never - > = .init([]) - - /// The mediator routing DID if one is currently registered. - public var mediatorRoutingDID: DID? { - connectionManager?.mediationHandler.mediator?.routingDID - } - let logger = SDKLogger(category: .edgeAgent) public let apollo: Apollo & KeyRestoration public let castor: Castor public let pluto: Pluto public let pollux: Pollux & CredentialImporter - public let mercury: Mercury - var mediationHandler: MediatorHandler? - - var connectionManager: ConnectionsManagerImpl? - var cancellables = [AnyCancellable]() - // Not a "stream" - var messagesStreamTask: Task? public static func setupLogging(logLevels: [LogComponent: LogLevel]) { SDKLogger.logLevels = logLevels @@ -63,26 +35,13 @@ public class EdgeAgent { castor: Castor, pluto: Pluto, pollux: Pollux & CredentialImporter, - mercury: Mercury, - mediationHandler: MediatorHandler? = nil, seed: Seed? = nil ) { self.apollo = apollo self.castor = castor self.pluto = pluto self.pollux = pollux - self.mercury = mercury self.seed = seed ?? apollo.createRandomSeed().seed - self.mediationHandler = mediationHandler - mediationHandler.map { - self.connectionManager = ConnectionsManagerImpl( - castor: castor, - mercury: mercury, - pluto: pluto, - mediationHandler: $0, - pairings: [] - ) - } } /** @@ -92,109 +51,23 @@ public class EdgeAgent { - seedData: Optional seed data for creating a new seed. If not provided, a random seed will be generated. - mediatorServiceEnpoint: Optional DID representing the service endpoint of the mediator. If not provided, the default Prism mediator endpoint will be used. */ - public convenience init( - seedData: Data? = nil, - mediatorDID: DID - ) { + public convenience init(seedData: Data? = nil) { let apollo = ApolloBuilder().build() let castor = CastorBuilder(apollo: apollo).build() let pluto = PlutoBuilder().build() let pollux = PolluxBuilder(pluto: pluto, castor: castor).build() - let secretsStream = createSecretsStream( - keyRestoration: apollo, - pluto: pluto, - castor: castor - ) - - let mercury = MercuryBuilder( - castor: castor, - secretsStream: secretsStream - ).build() - let seed = seedData.map { Seed(value: $0) } ?? apollo.createRandomSeed().seed self.init( apollo: apollo, castor: castor, pluto: pluto, pollux: pollux, - mercury: mercury, - mediationHandler: BasicMediatorHandler( - mediatorDID: mediatorDID, - mercury: mercury, - store: BasicMediatorHandler.PlutoMediatorStoreImpl(pluto: pluto) - ), seed: seed ) } - public func setupMediatorHandler(mediationHandler: MediatorHandler) async throws { - try await stop() - self.mediationHandler = mediationHandler - self.connectionManager = ConnectionsManagerImpl( - castor: castor, - mercury: mercury, - pluto: pluto, - mediationHandler: mediationHandler, - pairings: [] - ) - } - - public func setupMediatorDID(did: DID) async throws { - let mediatorHandler = BasicMediatorHandler( - mediatorDID: did, - mercury: mercury, - store: BasicMediatorHandler.PlutoMediatorStoreImpl(pluto: pluto) - ) - try await setupMediatorHandler(mediationHandler: mediatorHandler) - } - - /** - Start the EdgeAgent and Mediator services - - - Throws: EdgeAgentError.noMediatorAvailableError if no mediator is available, - as well as any error thrown by `createNewPeerDID` and `connectionManager.registerMediator` - */ - public func start() async throws { - guard - let connectionManager, - state == .stoped - else { return } - logger.info(message: "Starting agent") - state = .starting - do { - try await connectionManager.startMediator() - } catch EdgeAgentError.noMediatorAvailableError { - let hostDID = try await createNewPeerDID(updateMediator: false) - try await connectionManager.registerMediator(hostDID: hostDID) - } - try await firstLinkSecretSetup() - state = .running - logger.info(message: "Mediation Achieved", metadata: [ - .publicMetadata(key: "Routing DID", value: mediatorRoutingDID?.string ?? "") - ]) - logger.info(message: "Agent running") - } - - /** - This function is used to stop the EdgeAgent. - The function sets the state of EdgeAgent to .stoping. - All ongoing events that was created by the EdgeAgent are stopped. - After all the events are stopped the state of the EdgeAgent is set to .stoped. - - - Throws: If the current state is not running throws error. - */ - public func stop() async throws { - guard state == .running else { return } - logger.info(message: "Stoping agent") - state = .stoping - cancellables.forEach { $0.cancel() } - connectionManager?.stopAllEvents() - state = .stoped - logger.info(message: "Agent not running") - } - - private func firstLinkSecretSetup() async throws { + func firstLinkSecretSetup() async throws { if try await pluto.getLinkSecret().first().await() == nil { let secret = try apollo.createNewLinkSecret() guard let storableSecret = secret.storable else { @@ -213,57 +86,3 @@ extension DID { return str } } - -private func createSecretsStream( - keyRestoration: KeyRestoration, - pluto: Pluto, - castor: Castor -) -> AnyPublisher<[Secret], Error> { - pluto.getAllKeys() - .first() - .flatMap { keys in - Future { - let privateKeys = await keys.asyncMap { - try? await keyRestoration.restorePrivateKey($0) - }.compactMap { $0 } - return try parsePrivateKeys( - privateKeys: privateKeys, - castor: castor - ) - } - } - .eraseToAnyPublisher() -} - -private func parsePrivateKeys( - privateKeys: [PrivateKey], - castor: Castor -) throws -> [Domain.Secret] { - return try privateKeys - .map { $0 as? (PrivateKey & ExportableKey & StorableKey) } - .compactMap { $0 } - .map { privateKey in - return privateKey - } - .map { privateKey in - try parseToSecret( - privateKey: privateKey, - identifier: privateKey.identifier - ) - } -} - -private func parseToSecret(privateKey: PrivateKey & ExportableKey, identifier: String) throws -> Domain.Secret { - let jwk = privateKey.jwk - guard - let dataJson = try? JSONEncoder().encode(jwk), - let stringJson = String(data: dataJson, encoding: .utf8) - else { - throw CommonError.invalidCoding(message: "Could not encode privateKey.jwk") - } - return .init( - id: identifier, - type: .jsonWebKey2020, - secretMaterial: .jwk(value: stringJson) - ) -} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift index 2a7d51be..f8894a66 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgentErrors.swift @@ -9,6 +9,67 @@ import Domain */ public enum EdgeAgentError: KnownPrismError { + public enum OIDCError: KnownPrismError { + /// An error case representing that a `CredentialOffer` doesnt contain grants. + case noGrantProvided + + /// An error case representing that a flow is not supported. + case flowNotSupported + + /// An error case representing an internal error normally originating from a library + case internalError(error: Error) + + /// An error case representing an invalid callback url + case invalidCallbackURL + + /// An error case representing that some parameters are missing from the callback url + case missingQueryParameters(parameters: [String]) + + /// An error case representing that a credential request cannot be of deferred type + case crendentialResponseDeferredNotSupported + + /// An error case representing that an invalid proof was submited + case invalidProof(errorDescription: String?) + + public var code: Int { + switch self { + case .noGrantProvided: + return 1001 + case .flowNotSupported: + return 1002 + case .internalError: + return 1003 + case .invalidCallbackURL: + return 1004 + case .missingQueryParameters: + return 1005 + case .crendentialResponseDeferredNotSupported: + return 1006 + case .invalidProof: + return 1007 + } + } + + public var message: String { + switch self { + case .noGrantProvided: + return "No grant was provided" + case .flowNotSupported: + return "The flow is not supported" + case .internalError(error: let error): + return "An internal error occurred: \(error)" + case .invalidCallbackURL: + return "The callback url is invalid" + case .missingQueryParameters(parameters: let parameters): + return "The following query parameters are missing on the callback url: \(parameters)" + case .crendentialResponseDeferredNotSupported: + return "Credential response deferred is not supported" + case .invalidProof(errorDescription: let errorDescription): + return "The proof is invalid: \(errorDescription ?? "")" + } + } + } + /// An error case representing that a DID key pair index could not be found. case cannotFindDIDKeyPairIndex diff --git a/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift new file mode 100644 index 00000000..c8f693ac --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift @@ -0,0 +1,43 @@ +import Combine +import Domain +import Foundation + +// MARK: DID High Level functionalities +public extension OIDCAgent { + + /// This method create a new Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - services: an array of services associated to the DID + /// - Returns: The new created DID + func createNewPrismDID( + keyPathIndex: Int? = nil, + alias: String? = nil, + services: [DIDDocument.Service] = [] + ) async throws -> DID { + try await edgeAgent.createNewPrismDID( + keyPathIndex: keyPathIndex, + alias: alias, + services: services + ) + } + + /// This method registers a Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - did: the DID which will be registered. + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - Returns: The new created DID + func registerPrismDID( + did: DID, + privateKey: PrivateKey, + alias: String? = nil + ) async throws { + try await edgeAgent.registerPrismDID( + did: did, + privateKey: privateKey, + alias: alias + ) + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent.swift b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent.swift new file mode 100644 index 00000000..f9cf5f06 --- /dev/null +++ b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent.swift @@ -0,0 +1,198 @@ +import Core +import Domain +import Foundation +import OpenID4VCI +import JSONWebKey + +public class OIDCAgent { + public let edgeAgent: EdgeAgent + public var apollo: Apollo & KeyRestoration { edgeAgent.apollo } + public var castor: Castor { edgeAgent.castor } + public var pluto: Pluto { edgeAgent.pluto } + public var pollux: Pollux & CredentialImporter { edgeAgent.pollux } + var logger: SDKLogger { edgeAgent.logger } + + /// Initializes a EdgeAgent with the given dependency objects and seed data. + /// + /// - Parameters: + /// - apollo: An instance of Apollo. + /// - castor: An instance of Castor. + /// - pluto: An instance of Pluto. + /// - pollux: An instance of Pollux. + /// - mercury: An instance of Mercury. + /// - seed: A unique seed used to generate the unique DID. + /// - mediatorServiceEnpoint: The endpoint of the Mediator service to use. + public init( + edgeAgent: EdgeAgent + ) { + self.edgeAgent = edgeAgent + } + + /** + Convenience initializer for `EdgeAgent` that allows for optional initialization of seed data and mediator service endpoint. + + - Parameters: + - seedData: Optional seed data for creating a new seed. If not provided, a random seed will be generated. + - mediatorServiceEnpoint: Optional DID representing the service endpoint of the mediator. If not provided, the default Prism mediator endpoint will be used. + */ + public convenience init( + seedData: Data? = nil + ) { + let edgeAgent = EdgeAgent(seedData: seedData) + + self.init(edgeAgent: edgeAgent) + } + + public func parseCredentialOffer( + offerUri: String + ) async throws -> CredentialOffer { + let result = try await CredentialOfferRequestResolver() + .resolve(source: .init(urlString: offerUri)) + switch result { + case .success(let success): + return success + case .failure(let failure): + throw failure + } + } + + public func createAuthorizationRequest( + clientId: String, + redirectUri: URL, + offer: CredentialOffer + ) async throws -> (Issuer, UnauthorizedRequest) { + let config = OpenId4VCIConfig( + clientId: clientId, + authFlowRedirectionURI: redirectUri, + authorizeIssuanceConfig: .favorScopes, + usePAR: false + ) + + guard let grants = offer.grants else { + throw EdgeAgentError.OIDCError.noGrantProvided + } + + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config + ) + + switch grants { + case .authorizationCode: + let parPlaced = try await issuer.pushAuthorizationCodeRequest( + credentialOffer: offer + ) + switch parPlaced { + case .success(let success): + return (issuer, success) + case .failure(let failure): + throw EdgeAgentError.OIDCError.internalError(error: failure) + } + default: + throw EdgeAgentError.OIDCError.flowNotSupported + } + } + + public func handleTokenRequest( + request: UnauthorizedRequest, + issuer: Issuer, + callbackUrl: URL + ) async throws -> (Issuer, AuthorizedRequest){ + guard let components = URLComponents(url: callbackUrl, resolvingAgainstBaseURL: false) else { + throw EdgeAgentError.OIDCError.invalidCallbackURL + } + guard + let queryItems = components.queryItems, + let code = queryItems.first(where: { $0.name == "code" })?.value + else { + throw EdgeAgentError.OIDCError.missingQueryParameters(parameters: ["code"]) + } + + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: code) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let request): + let authorizedRequest = await issuer.requestAccessToken(authorizationCode: request) + + if case let .success(authorized) = authorizedRequest { + return (issuer, authorized) + } + + case .failure(let error): + throw EdgeAgentError.OIDCError.internalError(error: error) + } + + throw UnknownError.somethingWentWrongError(customMessage: "OIDC Flow did not complete successfully", underlyingErrors: nil) + + } + + public func credentialRequest( + issuer: Issuer, + offer: CredentialOffer, + request: AuthorizedRequest + ) async throws -> Credential { + let payload: IssuanceRequestPayload = .configurationBased( + credentialConfigurationIdentifier: offer.credentialConfigurationIdentifiers.first!, + claimSet: nil + ) + + let did = try await createNewPrismDID() + + guard + let keys = try await pluto.getDIDPrivateKeys(did: did).first().await(), + let key = try await keys.first.asyncMap({ try await apollo.restorePrivateKey($0) }), + let exported = key.exporting?.jwkWithKid(kid: did.string + "#authentication0") + else { + throw EdgeAgentError.cannotFindDIDKeyPairIndex + } + + let privateJwk = try exported.toJoseJWK() + let result = try await issuer.requestSingle( + proofRequest: request, + bindingKey: .jwk( + algorithm: .init(name: "ES256K"), + jwk: privateJwk.publicKey, + privateKey: privateJwk, + issuer: did.string + ), + requestPayload: payload) { issuerResponseEncryptionMetadata in + Issuer.createResponseEncryptionSpec(issuerResponseEncryptionMetadata) + } + + switch result { + case .success(let success): + switch success { + case .success(let response): + guard let credential = response.credentialResponses.first else { + throw UnknownError.somethingWentWrongError(customMessage: nil, underlyingErrors: nil) + } + switch credential { + case .deferred: + throw EdgeAgentError.OIDCError.crendentialResponseDeferredNotSupported + case .issued(let credential, _): + let parsedCredential = try await pollux.importCredential( + credentialData: credential.tryToData(), + restorationType: "jwt", + options: [] + ) + if let storableCredential = parsedCredential.storable { + try await pluto.storeCredential(credential: storableCredential).first().await() + } + + return parsedCredential + } + case .failed(let error): + throw EdgeAgentError.OIDCError.internalError(error: error) + case .invalidProof(_, let errorDescription): + throw EdgeAgentError.OIDCError.invalidProof(errorDescription: errorDescription) + } + case .failure(let failure): + throw EdgeAgentError.OIDCError.internalError(error: failure) + } + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift b/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift index 592ad376..a32cb71c 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/AnoncredsPresentationFlowTest.swift @@ -13,19 +13,19 @@ final class AnoncredsPresentationFlowTest: XCTestCase { var castor: Castor! var pollux: (Pollux & CredentialImporter)! var mercury = MercuryStub() - var edgeAgent: EdgeAgent! + var edgeAgent: DIDCommAgent! var linkSecret: Key! override func setUp() async throws { castor = CastorBuilder(apollo: apollo).build() pollux = PolluxBuilder(pluto: pluto, castor: castor).build() - edgeAgent = EdgeAgent( + let edgeAgent = EdgeAgent( apollo: apollo, castor: castor, pluto: pluto, - pollux: pollux, - mercury: mercury + pollux: pollux ) + self.edgeAgent = DIDCommAgent(edgeAgent: edgeAgent, mercury: mercury) linkSecret = try apollo.createNewLinkSecret() } @@ -51,7 +51,8 @@ final class AnoncredsPresentationFlowTest: XCTestCase { let presentation = try await edgeAgent.createPresentationForRequestProof( request: presentationRequest, - credential: credential + credential: credential, + options: [.disclosingClaims(claims: ["test"])] ) let verification = try await edgeAgent.pollux.verifyPresentation( diff --git a/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift index af4f4931..bc74b39b 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift @@ -23,7 +23,6 @@ final class BackupWalletTests: XCTestCase { castor: castor, pluto: pluto, pollux: pollux, - mercury: MercuryStub(), seed: seed ) return (agent, pluto) @@ -39,7 +38,6 @@ final class BackupWalletTests: XCTestCase { castor: castor, pluto: pluto, pollux: pollux, - mercury: MercuryStub(), seed: seed ) return (agent, pluto) @@ -47,7 +45,7 @@ final class BackupWalletTests: XCTestCase { func testBackup() async throws { let (backupAgent, backupPluto) = try createAgent() - _ = try await backupAgent.createNewPeerDID(updateMediator: false) + _ = try await backupAgent.createNewPrismDID() _ = try await backupAgent.createNewPrismDID() backupPluto.didPairs = [ diff --git a/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift b/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift index ad583a2b..838a3b44 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift @@ -29,7 +29,7 @@ final class CheckTests: XCTestCase { func testOOB() async throws { let oob = "https://mediator.rootsid.cloud?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNzk0Mjc4MzctY2MwNi00ODUzLWJiMzktNjg2ZWFjM2U2YjlhIiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNtczU1NVloRnRobjFXVjhjaURCcFptODZoSzl0cDgzV29qSlVteFBHazFoWi5WejZNa21kQmpNeUI0VFM1VWJiUXc1NHN6bTh5dk1NZjFmdEdWMnNRVllBeGFlV2hFLlNleUpwWkNJNkltNWxkeTFwWkNJc0luUWlPaUprYlNJc0luTWlPaUpvZEhSd2N6b3ZMMjFsWkdsaGRHOXlMbkp2YjNSemFXUXVZMnh2ZFdRaUxDSmhJanBiSW1ScFpHTnZiVzB2ZGpJaVhYMCIsImJvZHkiOnsiZ29hbF9jb2RlIjoicmVxdWVzdC1tZWRpYXRlIiwiZ29hbCI6IlJlcXVlc3RNZWRpYXRlIiwibGFiZWwiOiJNZWRpYXRvciIsImFjY2VwdCI6WyJkaWRjb21tL3YyIl19fQ" - let agent = EdgeAgent(mediatorDID: DID(method: "peer", methodId: "123")) + let agent = DIDCommAgent(mediatorDID: DID(method: "peer", methodId: "123")) let parsed = try await agent.parseInvitation(str: oob) print(parsed) diff --git a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift index 3fd0f109..46cd3e35 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/Helper/MockPollux.swift @@ -25,6 +25,30 @@ struct MockCredential: Credential, StorableCredential, ExportableCredential { } struct MockPollux: Pollux & CredentialImporter { + func parseCredential( + type: String, + credentialPayload: Data, + options: [Domain.CredentialOperationsOptions] + ) async throws -> any Domain.Credential { + return MockCredential(exporting: Data(count: 5), restorationType: "mocked") + } + + func processCredentialRequest( + type: String, + offerPayload: Data, + options: [Domain.CredentialOperationsOptions] + ) async throws -> String { + "" + } + + func verifyPresentation( + type: String, + presentationPayload: Data, + options: [Domain.CredentialOperationsOptions] + ) async throws -> Bool { + false + } + func importCredential( credentialData: Data, restorationType: String, diff --git a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift index 546961cb..80c28cb7 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift @@ -15,19 +15,19 @@ final class PresentationExchangeFlowTests: XCTestCase { var castor: Castor! var pollux: (Pollux & CredentialImporter)! var mercury = MercuryStub() - var edgeAgent: EdgeAgent! + var edgeAgent: DIDCommAgent! let logger = Logger(label: "presentation_exchange_test") override func setUp() async throws { castor = CastorBuilder(apollo: apollo).build() pollux = PolluxBuilder(pluto: pluto, castor: castor).build() - edgeAgent = EdgeAgent( + let edgeAgent = EdgeAgent( apollo: apollo, castor: castor, pluto: pluto, - pollux: pollux, - mercury: mercury + pollux: pollux ) + self.edgeAgent = DIDCommAgent(edgeAgent: edgeAgent, mercury: mercury) } func testJWTPresentationRequest() async throws { @@ -126,7 +126,8 @@ final class PresentationExchangeFlowTests: XCTestCase { let presentation = try await edgeAgent.createPresentationForRequestProof( request: message, - credential: credential + credential: credential, + options: [.disclosingClaims(claims: ["test"])] ) let verification = try await edgeAgent.pollux.verifyPresentation( diff --git a/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift index 1ec94d64..6e6af434 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift @@ -57,6 +57,43 @@ extension AnoncredsCredentialStack: ProvableCredential { } } + func presentation(type: String, requestPayload: Data, options: [CredentialOperationsOptions]) throws -> String { + let requestStr = try requestPayload.tryToString() + guard + let linkSecretOption = options.first(where: { + if case .linkSecret = $0 { return true } + return false + }), + case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "LinkSecret") + } + + if + let zkpParameters = options.first(where: { + if case .zkpPresentationParams = $0 { return true } + return false + }), + case let CredentialOperationsOptions.zkpPresentationParams(attributes, predicates) = zkpParameters + { + return try AnoncredsPresentation().createPresentation( + stack: self, + request: requestStr, + linkSecret: linkSecret, + attributes: attributes, + predicates: predicates + ) + } else { + return try AnoncredsPresentation().createPresentation( + stack: self, + request: requestStr, + linkSecret: linkSecret, + attributes: try computeAttributes(requestJson: requestStr), + predicates: try computePredicates(requestJson: requestStr) + ) + } + } + func isValidForPresentation(request: Message, options: [CredentialOperationsOptions]) throws -> Bool { guard let attachment = request.attachments.first @@ -71,6 +108,19 @@ extension AnoncredsCredentialStack: ProvableCredential { return false } } + + func isValidForPresentation( + type: String, + requestPayload: Data, + options: [CredentialOperationsOptions] + ) throws -> Bool { + switch type { + case "anoncreds/proof-request@v1.0": + return true + default: + return false + } + } } private func computeAttributes(requestJson: String) throws -> [String: Bool] { diff --git a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift index 934125e5..1d225cfd 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift @@ -4,9 +4,39 @@ import JSONWebToken extension JWTCredential: ProvableCredential { public func presentation(request: Message, options: [CredentialOperationsOptions]) throws -> String { + guard + let attachment = request.attachments.first, + let format = attachment.format, + let requestData = request.attachments.first.flatMap({ + switch $0.data { + case let json as AttachmentJsonData: + return json.data + case let bas64 as AttachmentBase64: + return Data(fromBase64URL: bas64.base64) + default: + return nil + } + }) + else { + throw PolluxError.offerDoesntProvideEnoughInformation + } + return try JWTPresentation().createPresentation( + credential: self, + type: format, + requestData: requestData, + options: options + ) + } + + public func presentation( + type: String, + requestPayload: Data, + options: [CredentialOperationsOptions] + ) throws -> String { try JWTPresentation().createPresentation( credential: self, - request: request, + type: type, + requestData: requestPayload, options: options ) } @@ -46,4 +76,33 @@ extension JWTCredential: ProvableCredential { throw PolluxError.unsupportedAttachmentFormat(attachment.format) } } + + public func isValidForPresentation( + type: String, + requestPayload: Data, + options: [CredentialOperationsOptions] + ) throws -> Bool { + switch type { + case "dif/presentation-exchange/definitions@v1.0": + let requestData = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: requestPayload) + let payload: Data = try JWT.getPayload(jwtString: jwtString) + guard + let format = requestData.presentationDefinition.format?.jwt + else { + return false + } + do { + try requestData.presentationDefinition.inputDescriptors.forEach { + try VerifyJsonClaim.verify(inputDescriptor: $0, jsonData: payload) + } + return true + } catch { + return false + } + case "prism/jwt", "jwt": + return true + default: + throw PolluxError.unsupportedAttachmentFormat(type) + } + } } diff --git a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential.swift index 3e617878..527a5a88 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential.swift @@ -40,12 +40,25 @@ extension JWTCredential: Credential { public var claims: [Claim] { guard - let dic = jwtVerifiableCredential.verifiableCredential.credentialSubject.value as? [String: String] + let dic = jwtVerifiableCredential.verifiableCredential.credentialSubject.value as? [String: Any] else { return [] } - return dic.map { - Claim(key: $0, value: .string($1)) + return dic.compactMap { + switch $1 { + case let value as Date: + Claim(key: $0, value: .date(value)) + case let value as Data: + Claim(key: $0, value: .data(value)) + case let value as Bool: + Claim(key: $0, value: .bool(value)) + case let value as String: + Claim(key: $0, value: .string(value)) + case let value as NSNumber: + Claim(key: $0, value: .number(value.doubleValue)) + default: + nil + } } } diff --git a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift index 47f48225..87b72441 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift @@ -38,7 +38,8 @@ struct JWTPresentation { func createPresentation( credential: JWTCredential, - request: Message, + type: String, + requestData: Data, options: [CredentialOperationsOptions] ) throws -> String { guard @@ -61,23 +62,7 @@ struct JWTPresentation { throw PolluxError.requiresExportableKeyForOperation(operation: "Create Presentation JWT Credential") } - guard - let attachment = request.attachments.first, - let requestData = request.attachments.first.flatMap({ - switch $0.data { - case let json as AttachmentJsonData: - return json.data - case let bas64 as AttachmentBase64: - return Data(fromBase64URL: bas64.base64) - default: - return nil - } - }) - else { - throw PolluxError.offerDoesntProvideEnoughInformation - } - - switch attachment.format { + switch type { case "dif/presentation-exchange/definitions@v1.0": return try presentation( credential: credential, diff --git a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT+ProvableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT+ProvableCredential.swift index 900cbf73..479f58a3 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT+ProvableCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT+ProvableCredential.swift @@ -3,14 +3,52 @@ import Foundation extension SDJWTCredential: ProvableCredential { func presentation(request: Domain.Message, options: [Domain.CredentialOperationsOptions]) throws -> String { + guard + let attachment = request.attachments.first, + let format = attachment.format, + let requestData = request.attachments.first.flatMap({ + switch $0.data { + case let json as AttachmentJsonData: + return json.data + case let bas64 as AttachmentBase64: + return Data(fromBase64URL: bas64.base64) + default: + return nil + } + }) + else { + throw PolluxError.offerDoesntProvideEnoughInformation + } + return try SDJWTPresentation().createPresentation( + credential: self, + type: format, + requestData: requestData, + options: options + ) + } + + func presentation( + type: String, + requestPayload: Data, + options: [CredentialOperationsOptions] + ) throws -> String { try SDJWTPresentation().createPresentation( credential: self, - request: request, + type: type, + requestData: requestPayload, options: options ) } - + func isValidForPresentation(request: Domain.Message, options: [Domain.CredentialOperationsOptions]) throws -> Bool { request.attachments.first.map { $0.format == "vc+sd-jwt"} ?? true } + + func isValidForPresentation( + type: String, + requestPayload: Data, + options: [CredentialOperationsOptions] + ) throws -> Bool { + type == "vc+sd-jwt" + } } diff --git a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift index 8f40590b..e2e6d3ce 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift @@ -7,7 +7,8 @@ import JSONWebKey struct SDJWTPresentation { func createPresentation( credential: SDJWTCredential, - request: Message, + type: String, + requestData: Data, options: [CredentialOperationsOptions] ) throws -> String{ guard @@ -34,23 +35,7 @@ struct SDJWTPresentation { disclosingClaims = [] } - guard - let attachment = request.attachments.first, - let requestData = request.attachments.first.flatMap({ - switch $0.data { - case let json as AttachmentJsonData: - return json.data - case let bas64 as AttachmentBase64: - return Data(fromBase64URL: bas64.base64) - default: - return nil - } - }) - else { - throw PolluxError.offerDoesntProvideEnoughInformation - } - - switch attachment.format { + switch type { case "dif/presentation-exchange/definitions@v1.0": return try presentation( credential: credential, diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift index 9fc7b695..1f5f3bc1 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift @@ -64,6 +64,37 @@ extension PolluxImpl { throw PolluxError.invalidCredentialError } + public func processCredentialRequest( + type: String, + offerPayload: Data, + options: [CredentialOperationsOptions] + ) async throws -> String { + switch type { + case "jwt", "prism/jwt": + return try await processJWTCredentialRequest(offerData: offerPayload, options: options) + case "vc+sd-jwt": + return try await processSDJWTCredentialRequest(offerData: offerPayload, options: options) + case "anoncreds/credential-offer@v1.0": + guard + let thidOption = options.first(where: { + if case .thid = $0 { return true } + return false + }), + case let CredentialOperationsOptions.thid(thid) = thidOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "thid") + } + return try await processAnoncredsCredentialRequest( + offerData: offerPayload, + thid: thid, + options: options + ) + default: + break + } + throw PolluxError.invalidCredentialError + } + private func processJWTCredentialRequest(offerData: Data, options: [CredentialOperationsOptions]) async throws -> String { guard let subjectDIDOption = options.first(where: { diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift index bbd3a051..9eb26aba 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift @@ -59,6 +59,38 @@ extension PolluxImpl { } } +//<<<<<<< HEAD +// private func getDefinition(id: String) async throws -> PresentationExchangeRequest { +//======= + public func verifyPresentation( + type: String, + presentationPayload: Data, + options: [CredentialOperationsOptions] + ) async throws -> Bool { + guard + let requestIdOption = options.first(where: { + if case .presentationRequestId = $0 { return true } + return false + }), + case let CredentialOperationsOptions.presentationRequestId(requestId) = requestIdOption + else { + throw PolluxError.invalidPrismDID + } + + switch type { + case "dif/presentation-exchange/submission@v1.0": + return try await verifyPresentationSubmission(json: presentationPayload, requestId: requestId) + case "anoncreds/proof@v1.0": + return try await verifyAnoncreds( + presentation: presentationPayload, + requestId: requestId, + options: options + ) + default: + throw PolluxError.unsupportedAttachmentFormat(type) + } + } + private func getDefinition(id: String) async throws -> PresentationExchangeRequest { guard let request = try await pluto.getMessage(id: id).first().await(), @@ -80,54 +112,20 @@ extension PolluxImpl { return try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: json) } - private func verifyJWTs(credentials: [String]) async throws { - var errors = [Error]() - await credentials - .asyncForEach { - do { - try await verifyJWT(jwtString: $0) - } catch { - errors.append(error) - } - } - guard errors.isEmpty else { - throw PolluxError.cannotVerifyPresentationInputs(errors: errors) - } - } - - private func verifyJWT(jwtString: String) async throws -> Bool { - try await verifyJWTCredentialRevocation(jwtString: jwtString) - let payload: DefaultJWTClaimsImpl = try JWT.getPayload(jwtString: jwtString) - guard let issuer = payload.iss else { - throw PolluxError.requiresThatIssuerExistsAndIsAPrismDID + private func verifyPresentationSubmission(json: Data, requestId: String) async throws -> Bool { + let presentationContainer = try JSONDecoder.didComm().decode(PresentationContainer.self, from: json) + let presentationRequest = try await getDefinition(id: requestId) + guard let submission = presentationContainer.presentationSubmission else { + throw PolluxError.presentationSubmissionNotAvailable } - let issuerDID = try DID(string: issuer) - let issuerKeys = try await castor.getDIDPublicKeys(did: issuerDID) - - ES256KVerifier.bouncyCastleFailSafe = true - - let validations = issuerKeys - .compactMap(\.exporting) - .compactMap { - try? JWT.verify(jwtString: jwtString, senderKey: $0.jwk.toJoseJWK()) - } - ES256KVerifier.bouncyCastleFailSafe = false - return !validations.isEmpty - } - - private func verifyJWTCredentialRevocation(jwtString: String) async throws { - guard let credential = try? JWTCredential(data: jwtString.tryToData()) else { - return - } - let isRevoked = try await credential.isRevoked - let isSuspended = try await credential.isSuspended - guard !isRevoked else { - throw PolluxError.credentialIsRevoked(jwtString: jwtString) - } - guard !isSuspended else { - throw PolluxError.credentialIsSuspended(jwtString: jwtString) - } + return try await VerifyPresentationSubmission( + castor: castor, + parsers: presentationExchangeParsers + ).verifyPresentationSubmission( + json: json, + presentationRequest: presentationRequest + ) } private func verifyAnoncreds( diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift index 7d359733..bb0553ad 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift @@ -86,4 +86,63 @@ extension PolluxImpl { throw PolluxError.invalidCredentialError } } + + public func parseCredential(type: String, credentialPayload: Data, options: [CredentialOperationsOptions]) async throws -> Credential { + switch type { + case "jwt", "prism/jwt": + return try ParseJWTCredentialFromMessage.parse(issuerCredentialData: credentialPayload) + case "vc+sd-jwt": + return try SDJWTCredential(sdjwtString: credentialPayload.tryToString()) + case "anoncreds", "prism/anoncreds", "anoncreds/credential@v1.0": + guard + let thidOption = options.first(where: { + if case .thid = $0 { return true } + return false + }), + case let CredentialOperationsOptions.thid(thid) = thidOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "thid") + } + guard + let linkSecretOption = options.first(where: { + if case .linkSecret = $0 { return true } + return false + }), + case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "linkSecret") + } + + guard + let credDefinitionDownloaderOption = options.first(where: { + if case .credentialDefinitionDownloader = $0 { return true } + return false + }), + case let CredentialOperationsOptions.credentialDefinitionDownloader(definitionDownloader) = credDefinitionDownloaderOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "credentialDefinitionDownloader") + } + + guard + let schemaDownloaderOption = options.first(where: { + if case .schemaDownloader = $0 { return true } + return false + }), + case let CredentialOperationsOptions.schemaDownloader(schemaDownloader) = schemaDownloaderOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "schemaDownloader") + } + + return try await ParseAnoncredsCredentialFromMessage.parse( + issuerCredentialData: credentialPayload, + linkSecret: linkSecret, + credentialDefinitionDownloader: definitionDownloader, + schemaDownloader: schemaDownloader, + thid: thid, + pluto: self.pluto + ) + default: + throw PolluxError.invalidCredentialError + } + } } diff --git a/Package.swift b/Package.swift index 3782109f..542c1fb9 100644 --- a/Package.swift +++ b/Package.swift @@ -56,15 +56,16 @@ let package = Package( from: "1.4.4" ), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.7.0"), - .package(url: "https://github.com/beatt83/didcomm-swift.git", from: "0.1.8"), - .package(url: "https://github.com/beatt83/jose-swift.git", from: "3.2.0"), + .package(url: "https://github.com/beatt83/didcomm-swift.git", from: "0.1.10"), + .package(url: "https://github.com/beatt83/jose-swift.git", from: "3.3.1"), .package(url: "https://github.com/beatt83/peerdid-swift.git", from: "3.0.1"), .package(url: "https://github.com/input-output-hk/anoncreds-rs.git", exact: "0.4.1"), .package(url: "https://github.com/hyperledger/identus-apollo.git", exact: "1.4.2"), .package(url: "https://github.com/KittyMac/Sextant.git", exact: "0.4.31"), .package(url: "https://github.com/kylef/JSONSchema.swift.git", exact: "0.6.0"), - .package(url: "https://github.com/goncalo-frade-iohk/eudi-lib-sdjwt-swift.git", from: "0.0.2"), - .package(url: "https://github.com/1024jp/GzipSwift.git", exact: "6.0.0") + .package(url: "https://github.com/eu-digital-identity-wallet/eudi-lib-sdjwt-swift.git", from: "0.1.0"), + .package(url: "https://github.com/1024jp/GzipSwift.git", exact: "6.0.0"), + .package(url: "https://github.com/goncalo-frade-iohk/eudi-lib-ios-openid4vci-swift.git", branch: "feature/add-w3cvc-support") ], targets: [ .target( @@ -171,7 +172,8 @@ let package = Package( dependencies: [ "Domain", "Builders", - "Core" + "Core", + .product(name: "OpenID4VCI", package: "eudi-lib-ios-openid4vci-swift") ], path: "EdgeAgentSDK/EdgeAgent/Sources" ), diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj index f9fe724d..f1fc6c9c 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj @@ -34,6 +34,7 @@ EE418AF72BCFD926008766A6 /* CreatePresentationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE418AEB2BCFD925008766A6 /* CreatePresentationView.swift */; }; EE549F472ACC1F5E0038ED1D /* CredentialDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE549F462ACC1F5E0038ED1D /* CredentialDetailView.swift */; }; EE549F492ACC1F7D0038ED1D /* CredentialDetailViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE549F482ACC1F7D0038ED1D /* CredentialDetailViewState.swift */; }; + EE566EDF2C9C4708004C4051 /* DeepLinkWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE566EDE2C9C4708004C4051 /* DeepLinkWebView.swift */; }; EE6C38DC294626E1006CD2D3 /* String+extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE6C38DB294626E1006CD2D3 /* String+extensions.swift */; }; EE6C38E3294627B2006CD2D3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = EE6C38E2294627B2006CD2D3 /* Localizable.strings */; }; EE6C38E529462822006CD2D3 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = EE6C38E429462822006CD2D3 /* Localizable.stringsdict */; }; @@ -165,6 +166,7 @@ EE418AEB2BCFD925008766A6 /* CreatePresentationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreatePresentationView.swift; sourceTree = ""; }; EE549F462ACC1F5E0038ED1D /* CredentialDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialDetailView.swift; sourceTree = ""; }; EE549F482ACC1F7D0038ED1D /* CredentialDetailViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialDetailViewState.swift; sourceTree = ""; }; + EE566EDE2C9C4708004C4051 /* DeepLinkWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLinkWebView.swift; sourceTree = ""; }; EE6C38DB294626E1006CD2D3 /* String+extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+extensions.swift"; sourceTree = ""; }; EE6C38E2294627B2006CD2D3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; EE6C38E429462822006CD2D3 /* Localizable.stringsdict */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; @@ -512,6 +514,7 @@ EE92C3FA2B7E1CA200FC0B6E /* Environment */, EE92C3FD2B7E1CA200FC0B6E /* PreferenceHelpers */, EE92C4002B7E1CA200FC0B6E /* NavigationUtils.swift */, + EE566EDE2C9C4708004C4051 /* DeepLinkWebView.swift */, ); path = AtalaSwiftUIComponents; sourceTree = ""; @@ -931,6 +934,7 @@ EE92C40C2B7E1CA200FC0B6E /* ClearFullCoverModifier.swift in Sources */, EE92C41B2B7E1CA200FC0B6E /* WordTagGrid.swift in Sources */, EE418AED2BCFD926008766A6 /* PresentationDetailViewModel.swift in Sources */, + EE566EDF2C9C4708004C4051 /* DeepLinkWebView.swift in Sources */, EE75147E29C376E700FFFAA4 /* DIDDetailView.swift in Sources */, EEE61FE02937CEAA0053AE52 /* SeedViewModel.swift in Sources */, EE92C41E2B7E1CA200FC0B6E /* WebView.swift in Sources */, @@ -1164,6 +1168,7 @@ INFOPLIST_FILE = AtalaPrismWalletDemo/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Atala PRISM Wallet Demo"; INFOPLIST_KEY_NSCameraUsageDescription = NSCameraUsageDescription; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -1196,6 +1201,7 @@ INFOPLIST_FILE = AtalaPrismWalletDemo/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Atala PRISM Wallet Demo"; INFOPLIST_KEY_NSCameraUsageDescription = NSCameraUsageDescription; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/DeepLinkWebView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/DeepLinkWebView.swift new file mode 100644 index 00000000..d8e1d498 --- /dev/null +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Helper/AtalaSwiftUIComponents/DeepLinkWebView.swift @@ -0,0 +1,50 @@ +import SwiftUI +import WebKit + +struct DeepLinkWebView: UIViewRepresentable { + let url: URL + @Binding var deepLinkUrl: URL? + @Binding var shouldDismiss: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIView(context: Context) -> WKWebView { + let webView = WKWebView() + webView.navigationDelegate = context.coordinator + + let request = URLRequest(url: url) + webView.load(request) + + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + // No need to update the view + } + + class Coordinator: NSObject, WKNavigationDelegate { + var parent: DeepLinkWebView + + init(_ parent: DeepLinkWebView) { + self.parent = parent + } + + // Intercept URL changes + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + + if let url = navigationAction.request.url { + // Check if the URL matches your deep link scheme + if url.scheme == "edgeagentsdk" { + parent.deepLinkUrl = url + parent.shouldDismiss = false + decisionHandler(.cancel) + return + } + } + decisionHandler(.allow) + } + } +} diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Info.plist b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Info.plist index 67e5f5ef..770900cf 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Info.plist +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Info.plist @@ -14,18 +14,21 @@ walletX + + CFBundleTypeRole + Editor + CFBundleURLName + com.identus.oidc + CFBundleURLSchemes + + edgeagentsdk + + NSAppTransportSecurity NSAllowsArbitraryLoads - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/DID/DIDFuncionalitiesViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/DID/DIDFuncionalitiesViewModel.swift index f7117125..52b7a7b0 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/DID/DIDFuncionalitiesViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/DID/DIDFuncionalitiesViewModel.swift @@ -5,13 +5,13 @@ import EdgeAgent final class DIDFuncionalitiesViewModel: ObservableObject { private let castor: Castor - private let agent: EdgeAgent + private let agent: DIDCommAgent init() { self.castor = CastorBuilder( apollo: ApolloBuilder().build() ).build() - self.agent = EdgeAgent(mediatorDID: DID(method: "peer", methodId: "123")) + self.agent = DIDCommAgent(mediatorDID: DID(method: "peer", methodId: "123")) } @Published var createdDID: DID? diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SetupPrismAgent/SetupPrismAgentViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SetupPrismAgent/SetupPrismAgentViewModel.swift index 8cb3b190..d952c3e5 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SetupPrismAgent/SetupPrismAgentViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SetupPrismAgent/SetupPrismAgentViewModel.swift @@ -10,13 +10,13 @@ final class SetupEdgeAgentViewModelImpl: ObservableObject, SetupEdgeAgentViewMod @Published var status: String = "" @Published var error: String? - private let agent: EdgeAgent + private let agent: DIDCommAgent private var cancellables = [AnyCancellable]() init() { let did = try! DID(string: "did:peer:2.Ez6LSms555YhFthn1WV8ciDBpZm86hK9tp83WojJUmxPGk1hZ.Vz6MkmdBjMyB4TS5UbbQw54szm8yvMMf1ftGV2sQVYAxaeWhE.SeyJpZCI6Im5ldy1pZCIsInQiOiJkbSIsInMiOiJodHRwczovL21lZGlhdG9yLnJvb3RzaWQuY2xvdWQiLCJhIjpbImRpZGNvbW0vdjIiXX0") - self.agent = EdgeAgent(mediatorDID: did) + self.agent = DIDCommAgent(mediatorDID: did) status = agent.state.rawValue } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SigningVerification/SigningVerificationViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SigningVerification/SigningVerificationViewModel.swift index faf7d303..8656931d 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SigningVerification/SigningVerificationViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/SigningVerification/SigningVerificationViewModel.swift @@ -5,13 +5,13 @@ import EdgeAgent final class SigningVerificationViewModel: ObservableObject { private let castor: Castor - private let agent: EdgeAgent + private let agent: DIDCommAgent init() { self.castor = CastorBuilder( apollo: ApolloBuilder().build() ).build() - self.agent = EdgeAgent(mediatorDID: DID(method: "peer", methodId: "1234")) + self.agent = DIDCommAgent(mediatorDID: DID(method: "peer", methodId: "1234")) } @Published var createdDID: DID? @@ -47,7 +47,7 @@ final class SigningVerificationViewModel: ObservableObject { else { return } // Signs with a valid DID that was created by the agent - let signature = try? await agent.signWith(did: did, message: messageData) + let signature = try? await agent.edgeAgent.signWith(did: did, message: messageData) await MainActor.run { self.signedMessage = signature } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Main/MainVerifierRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Main/MainVerifierRouter.swift index 223806f2..601d23fd 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Main/MainVerifierRouter.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Main/MainVerifierRouter.swift @@ -20,26 +20,28 @@ final class MainVerifierRouterImpl: MainVerifierViewRouter { castor: castor ) ).build() - let agent = EdgeAgent( + let edgeAgent = EdgeAgent( apollo: apollo, castor: castor, pluto: pluto, - pollux: pollux, - mercury: mercury + pollux: pollux ) + let didcommAgent = DIDCommAgent(edgeAgent: edgeAgent, mercury: mercury) + let oidcAgent = OIDCAgent(edgeAgent: edgeAgent) container.register(type: Apollo.self, component: apollo) container.register(type: Castor.self, component: castor) container.register(type: Pluto.self, component: pluto) container.register(type: Pollux.self, component: pollux) container.register(type: Mercury.self, component: mercury) - container.register(type: EdgeAgent.self, component: agent) + container.register(type: DIDCommAgent.self, component: didcommAgent) + container.register(type: OIDCAgent.self, component: oidcAgent) } func routeToMediator() -> some View { let viewModel = MediatorViewModelImpl( castor: container.resolve(type: Castor.self)!, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MediatorPageView(viewModel: viewModel) } @@ -47,7 +49,7 @@ final class MainVerifierRouterImpl: MainVerifierViewRouter { func routeToDids() -> some View { let viewModel = DIDListViewModelImpl( pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return DIDListView(viewModel: viewModel) @@ -57,7 +59,7 @@ final class MainVerifierRouterImpl: MainVerifierViewRouter { let viewModel = ConnectionsListViewModelImpl( castor: container.resolve(type: Castor.self)!, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return ConnectionsListView( @@ -68,7 +70,7 @@ final class MainVerifierRouterImpl: MainVerifierViewRouter { func routeToMessages() -> some View { let viewModel = MessagesListViewModelImpl( - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MessagesListView( @@ -88,7 +90,7 @@ final class MainVerifierRouterImpl: MainVerifierViewRouter { func routeToCredentials() -> some View { let viewModel = CredentialListViewModelImpl( - agent: container.resolve(type: EdgeAgent.self)!, + agent: container.resolve(type: DIDCommAgent.self)!, apollo: container.resolve(type: Apollo.self)! as! Apollo & KeyRestoration, pluto: container.resolve(type: Pluto.self)! ) diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationCreate/CreatePresentationViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationCreate/CreatePresentationViewModel.swift index 40e6f062..24b24289 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationCreate/CreatePresentationViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationCreate/CreatePresentationViewModel.swift @@ -10,9 +10,9 @@ class CreatePresentationViewModelImpl: CreatePresentationViewModel { @Published var selectedCredentialType: CreatePresentationViewState.CredentialType = .jwt @Published var jwtClaims: [CreatePresentationViewState.JWTClaim] = [] @Published var anoncredsClaims: [CreatePresentationViewState.AnoncredsClaim] = [] - private let agent: EdgeAgent + private let agent: DIDCommAgent - init(edgeAgent: EdgeAgent) { + init(edgeAgent: DIDCommAgent) { self.agent = edgeAgent bind() @@ -20,6 +20,7 @@ class CreatePresentationViewModelImpl: CreatePresentationViewModel { func bind() { agent + .edgeAgent .getAllDIDPairs() .map { $0.map { diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Presentations/PresentationsRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Presentations/PresentationsRouter.swift index afb0ee5b..5c45f9be 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Presentations/PresentationsRouter.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/Presentations/PresentationsRouter.swift @@ -6,7 +6,7 @@ struct PresentationsViewRouterImpl: PresentationsViewRouter { let container: DIContainer func routeToCreate() -> some View { - let viewModel = CreatePresentationViewModelImpl(edgeAgent: container.resolve(type: EdgeAgent.self)!) + let viewModel = CreatePresentationViewModelImpl(edgeAgent: container.resolve(type: DIDCommAgent.self)!) return CreatePresentationView(viewModel: viewModel) } @@ -14,7 +14,7 @@ struct PresentationsViewRouterImpl: PresentationsViewRouter { func routeToDetail(id: String) -> some View { let viewModel = PresentationDetailViewModelImpl( id: id, - agent: container.resolve(type: EdgeAgent.self)!, + agent: container.resolve(type: DIDCommAgent.self)!, pluto: container.resolve(type: Pluto.self)! ) diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationsDetail/PresentationDetailViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationsDetail/PresentationDetailViewModel.swift index 0bcb3c45..27d95879 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationsDetail/PresentationDetailViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/Verifier/PresentationsDetail/PresentationDetailViewModel.swift @@ -15,12 +15,12 @@ class PresentationDetailViewModelImpl: PresentationDetailViewModel { @Published var receivedPresentations: [PresentationDetailViewState.ReceivedPresentation] = [] @Published var isVerified = false - private let agent: EdgeAgent + private let agent: DIDCommAgent private let pluto: Pluto init( id: String, - agent: EdgeAgent, + agent: DIDCommAgent, pluto: Pluto ) { self.agent = agent diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift index 7eb8c64c..091336e2 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactBuilder.swift @@ -12,7 +12,8 @@ struct AddNewContactBuilder: Builder { let viewModel = getViewModel(component: component) { AddNewContactViewModelImpl( token: component.token ?? "", - agent: component.container.resolve(type: EdgeAgent.self)!, + agent: component.container.resolve(type: DIDCommAgent.self)!, + oidcAgent: component.container.resolve(type: OIDCAgent.self)!, pluto: component.container.resolve(type: Pluto.self)! ) } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift index 482269f0..1bb80d4a 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactView.swift @@ -7,11 +7,16 @@ protocol AddNewContactViewModel: ObservableObject { var dismiss: Bool { get } var dismissRoot: Bool { get } var code: String { get set } + var url: URL? { get } + var hasUrl: Bool { get set } func isContactAlreadyAdded() func addContact() + func handleLinkCallback(url: URL) async throws } struct AddNewContactView: View { + @State var shouldDismissWebView = false + @State var deepLinkUrl: URL? @StateObject var viewModel: ViewModel @Environment(\.presentationMode) var presentationMode @Environment(\.rootPresentationMode) var modalPresentation @@ -85,6 +90,28 @@ struct AddNewContactView: View { .onChange(of: viewModel.dismissRoot, perform: { value in self.modalPresentation.wrappedValue = value }) + .sheet( + isPresented: $viewModel.hasUrl, + onDismiss: { + if let deepLinkUrl { + Task { + try await self.viewModel.handleLinkCallback(url: deepLinkUrl) + } + } + }, + content: { + DeepLinkWebView( + url: viewModel.url!, + deepLinkUrl: $deepLinkUrl, + shouldDismiss: $viewModel.hasUrl + ) + }) + .onOpenURL(perform: { url in + Task { + print("App was opened via URL: \(url)") + try await self.viewModel.handleLinkCallback(url: url) + } + }) } } @@ -95,6 +122,10 @@ struct AddNewContactView_Previews: PreviewProvider { } private class MockViewModel: AddNewContactViewModel { + var url: URL? = nil + var hasUrl: Bool = false + + func handleLinkCallback(url: URL) async throws {} var contactInfo: AddNewContactState.Contact? var flowStep: AddNewContactState.AddContacFlowStep = .getCode var loading = false diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift index c0907dce..4f2c65e2 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/AddNewContact/AddNewContactViewModel.swift @@ -2,6 +2,7 @@ import Combine import Domain import Foundation import EdgeAgent +import OpenID4VCI final class AddNewContactViewModelImpl: AddNewContactViewModel { @Published var flowStep: AddNewContactState.AddContacFlowStep @@ -10,18 +11,26 @@ final class AddNewContactViewModelImpl: AddNewContactViewModel { @Published var dismissRoot = false @Published var loading = false @Published var contactInfo: AddNewContactState.Contact? + @Published var url: URL? + @Published var hasUrl: Bool = false private let pluto: Pluto - private let agent: EdgeAgent + private let agent: DIDCommAgent + private let oidcAgent: OIDCAgent private var cancellables = Set() + private var issuer: Issuer? + private var offer: CredentialOffer? + private var request: UnauthorizedRequest? init( token: String = "", - agent: EdgeAgent, + agent: DIDCommAgent, + oidcAgent: OIDCAgent, pluto: Pluto ) { code = token self.agent = agent + self.oidcAgent = oidcAgent self.pluto = pluto flowStep = token.isEmpty ? .getCode : .checkDuplication } @@ -36,7 +45,7 @@ final class AddNewContactViewModelImpl: AddNewContactViewModel { do { if let recipientDID = try? DID(string: self.code) { - let didPairs = try await agent.getAllDIDPairs().first().await() + let didPairs = try await agent.edgeAgent.getAllDIDPairs().first().await() await MainActor.run { [weak self] in guard didPairs.first(where: { $0.other.string == recipientDID.string }) == nil else { @@ -50,9 +59,26 @@ final class AddNewContactViewModelImpl: AddNewContactViewModel { self?.loading = false } + } else if self.code.contains("openid-credential-offer"){ + let offer = try await oidcAgent.parseCredentialOffer(offerUri: self.code) + self.offer = offer + let prePreparedRequest = try await oidcAgent.createAuthorizationRequest( + clientId: "alice-wallet", + redirectUri: URL(string: "edgeagentsdk://oidc")!, + offer: offer + ) + self.issuer = prePreparedRequest.0 + self.request = prePreparedRequest.1 + switch self.request { + case .par(let parRequested): + self.url = parRequested.getAuthorizationCodeURL.url + self.hasUrl = true + default: + throw UnknownError.somethingWentWrongError(customMessage: nil, underlyingErrors: nil) + } } else { let connection = try agent.parseOOBInvitation(url: self.code) - let didPairs = try await agent.getAllDIDPairs().first().await() + let didPairs = try await agent.edgeAgent.getAllDIDPairs().first().await() await MainActor.run { [weak self] in guard didPairs.first(where: { $0.other.string == connection.from }) == nil else { @@ -75,6 +101,23 @@ final class AddNewContactViewModelImpl: AddNewContactViewModel { } } + func handleLinkCallback(url: URL) async throws { + hasUrl = false + let response = try await oidcAgent.handleTokenRequest( + request: request!, + issuer: issuer!, + callbackUrl: url + ) + let credential = try await oidcAgent.credentialRequest( + issuer: response.0, + offer: offer!, + request: response.1 + ) + loading = false + dismiss = true + print(credential) + } + func addContact() { guard contactInfo != nil, !loading else { return } loading = true diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewBuilder.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewBuilder.swift index 16286dae..10183a27 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewBuilder.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewBuilder.swift @@ -8,7 +8,7 @@ struct BackupComponent: ComponentContainer { struct BackupBuilder: Builder { func build(component: BackupComponent) -> some View { let viewModel = getViewModel(component: component) { - BackupViewModelImpl(agent: component.container.resolve(type: EdgeAgent.self)!) + BackupViewModelImpl(agent: component.container.resolve(type: DIDCommAgent.self)!) } return BackupView(viewModel: viewModel) } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewModel.swift index c33eddb2..a505a518 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Backup/BackupViewModel.swift @@ -5,14 +5,14 @@ import Foundation final class BackupViewModelImpl: BackupViewModel { @Published var newJWE: String? = nil - private let agent: EdgeAgent + private let agent: DIDCommAgent - init(agent: EdgeAgent) { + init(agent: DIDCommAgent) { self.agent = agent } func createNewJWE() async throws { - let jwe = try await agent.backupWallet() + let jwe = try await agent.edgeAgent.backupWallet() await MainActor.run { self.newJWE = jwe @@ -21,7 +21,7 @@ final class BackupViewModelImpl: BackupViewModel { func backupWith(_ jwe: String) async throws { do { - try await agent.recoverWallet(encrypted: jwe) + try await agent.edgeAgent.recoverWallet(encrypted: jwe) } catch { print(error) print() diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListViewModel.swift index 56b0730c..a6edc0e0 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Connections/ConnectionsListViewModel.swift @@ -10,9 +10,9 @@ final class ConnectionsListViewModelImpl: ConnectionsListViewModel { private var cancellables = Set() private let castor: Castor private let pluto: Pluto - private let agent: EdgeAgent + private let agent: DIDCommAgent - init(castor: Castor, pluto: Pluto, agent: EdgeAgent) { + init(castor: Castor, pluto: Pluto, agent: DIDCommAgent) { self.castor = castor self.pluto = pluto self.agent = agent @@ -22,6 +22,7 @@ final class ConnectionsListViewModelImpl: ConnectionsListViewModel { func bind() { agent + .edgeAgent .getAllDIDPairs() .map { $0.map { diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailViewModel.swift index 62e584d6..29b12ca8 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialDetail/CredentialDetailViewModel.swift @@ -11,10 +11,10 @@ final class CredentialDetailViewModelImpl: CredentialDetailViewModel { schemaId: nil ) - private let agent: EdgeAgent + private let agent: DIDCommAgent private let credentialId: String - init(agent: EdgeAgent, credentialId: String) { + init(agent: DIDCommAgent, credentialId: String) { self.agent = agent self.credentialId = credentialId bind() @@ -22,7 +22,7 @@ final class CredentialDetailViewModelImpl: CredentialDetailViewModel { private func bind() { let credentialId = self.credentialId - return self.agent + return self.agent.edgeAgent .verifiableCredentials() .map { if let credential = $0.first(where: { $0.id == credentialId }) { diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListRouter.swift index 84eb8f7f..e239edf9 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListRouter.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListRouter.swift @@ -6,7 +6,7 @@ struct CredentialListRouterImpl: CredentialListRouter { func routeToCredentialDetail(id: String) -> some View { CredentialDetailView(viewModel: CredentialDetailViewModelImpl( - agent: container.resolve(type: EdgeAgent.self)!, + agent: container.resolve(type: DIDCommAgent.self)!, credentialId: id )) } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift index 42d30534..72ff60af 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift @@ -16,12 +16,12 @@ final class CredentialListViewModelImpl: CredentialListViewModel { @Published var invalidCredentials = [CredentialListViewState.Credential]() @Published var requestId: String? = nil - private let agent: EdgeAgent + private let agent: DIDCommAgent private let pluto: Pluto private let apollo: Apollo & KeyRestoration init( - agent: EdgeAgent, + agent: DIDCommAgent, apollo: Apollo & KeyRestoration, pluto: Pluto ) { @@ -32,7 +32,7 @@ final class CredentialListViewModelImpl: CredentialListViewModel { } private func bind() { - self.agent.verifiableCredentials().map { + self.agent.edgeAgent.verifiableCredentials().map { $0.map { CredentialListViewState.Credential( id: $0.id, @@ -52,6 +52,7 @@ final class CredentialListViewModelImpl: CredentialListViewModel { .dropNil() .flatMap { message in self.agent + .edgeAgent .verifiableCredentials() .map { $0.filter { (try? $0.proof?.isValidForPresentation(request: message, options: [])) ?? false} @@ -77,6 +78,7 @@ final class CredentialListViewModelImpl: CredentialListViewModel { .dropNil() .flatMap { message in self.agent + .edgeAgent .verifiableCredentials() .map { $0.filter { !((try? $0.proof?.isValidForPresentation(request: message, options: [])) ?? false)} @@ -100,7 +102,7 @@ final class CredentialListViewModelImpl: CredentialListViewModel { finalThreadFlowRequests() Task { - let credentials = try await self.agent.verifiableCredentials().first().await() + let credentials = try await self.agent.edgeAgent.verifiableCredentials().first().await() let linkSecret = try await self.agent.pluto.getLinkSecret().first().await() guard credentials.isEmpty, linkSecret != nil else { return @@ -146,7 +148,7 @@ final class CredentialListViewModelImpl: CredentialListViewModel { _ = try await self.agent.sendMessage(message: try requestCredential.makeMessage()) case ProtocolTypes.didcommRequestPresentation.rawValue: - let credential = try await self.agent.verifiableCredentials() + let credential = try await self.agent.edgeAgent.verifiableCredentials() .map { $0.compactMap { $0 as? Credential & ProvableCredential} } .map { $0.first { $0.id == credentialId } } .first() diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/DIDs/DIDList/DIDListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/DIDs/DIDList/DIDListViewModel.swift index a83888f6..76806284 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/DIDs/DIDList/DIDListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/DIDs/DIDList/DIDListViewModel.swift @@ -9,9 +9,9 @@ final class DIDListViewModelImpl: DIDListViewModel { @Published var error: FancyToast? private let pluto: Pluto - private let agent: EdgeAgent + private let agent: DIDCommAgent - init(pluto: Pluto, agent: EdgeAgent) { + init(pluto: Pluto, agent: DIDCommAgent) { self.pluto = pluto self.agent = agent diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift index b110bbe4..8cb1552e 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Main/Main2Router.swift @@ -34,22 +34,24 @@ final class Main2RouterImpl: Main2ViewRouter { castor: castor, pluto: pluto, pollux: pollux, - mercury: mercury, seed: seed ) + let didcommAgent = DIDCommAgent(edgeAgent: agent, mercury: mercury) + let oidcAgent = OIDCAgent(edgeAgent: agent) container.register(type: Apollo.self, component: apollo) container.register(type: Castor.self, component: castor) container.register(type: Pluto.self, component: pluto) container.register(type: Pollux.self, component: pollux) container.register(type: Mercury.self, component: mercury) - container.register(type: EdgeAgent.self, component: agent) + container.register(type: DIDCommAgent.self, component: didcommAgent) + container.register(type: OIDCAgent.self, component: oidcAgent) } func routeToMediator() -> some View { let viewModel = MediatorViewModelImpl( castor: container.resolve(type: Castor.self)!, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MediatorPageView(viewModel: viewModel) } @@ -57,7 +59,7 @@ final class Main2RouterImpl: Main2ViewRouter { func routeToDids() -> some View { let viewModel = DIDListViewModelImpl( pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return DIDListView(viewModel: viewModel) @@ -67,7 +69,7 @@ final class Main2RouterImpl: Main2ViewRouter { let viewModel = ConnectionsListViewModelImpl( castor: container.resolve(type: Castor.self)!, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return ConnectionsListView( @@ -78,7 +80,7 @@ final class Main2RouterImpl: Main2ViewRouter { func routeToMessages() -> some View { let viewModel = MessagesListViewModelImpl( - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MessagesListView( @@ -89,7 +91,7 @@ final class Main2RouterImpl: Main2ViewRouter { func routeToCredentials() -> some View { let viewModel = CredentialListViewModelImpl( - agent: container.resolve(type: EdgeAgent.self)!, + agent: container.resolve(type: DIDCommAgent.self)!, apollo: container.resolve(type: Apollo.self)! as! Apollo & KeyRestoration, pluto: container.resolve(type: Pluto.self)! ) diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift index 2fdff8a3..68630cea 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageViewModel.swift @@ -11,9 +11,9 @@ final class MediatorViewModelImpl: MediatorPageViewModel { @Published var error: FancyToast? private let castor: Castor private let pluto: Pluto - private let agent: EdgeAgent + private let agent: DIDCommAgent - init(castor: Castor, pluto: Pluto, agent: EdgeAgent) { + init(castor: Castor, pluto: Pluto, agent: DIDCommAgent) { self.castor = castor self.pluto = pluto self.agent = agent diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessageDetail/MessageDetailViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessageDetail/MessageDetailViewModel.swift index 61f7d2cb..657443ab 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessageDetail/MessageDetailViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessageDetail/MessageDetailViewModel.swift @@ -25,11 +25,11 @@ final class MessageDetailViewModelImpl: MessageDetailViewModel { private let messageId: String private let pluto: Pluto - private let agent: EdgeAgent + private let agent: DIDCommAgent private var message: Message? private var cancellables = Set() - init(messageId: String, pluto: Pluto, agent: EdgeAgent) { + init(messageId: String, pluto: Pluto, agent: DIDCommAgent) { self.messageId = messageId self.pluto = pluto self.agent = agent @@ -80,7 +80,7 @@ final class MessageDetailViewModelImpl: MessageDetailViewModel { case .didcommPresentation: let presentation = try Presentation(fromMessage: message) case .didcommRequestPresentation: - let credential = try await agent.verifiableCredentials().map { $0.first }.first().await() + let credential = try await agent.edgeAgent.verifiableCredentials().map { $0.first }.first().await() guard let credential else { throw UnknownError.somethingWentWrongError() } diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListRouter.swift index aa7fbb83..84387343 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListRouter.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListRouter.swift @@ -9,7 +9,7 @@ struct MessageListRouterImpl: MessageListRouter { let viewModel = MessageDetailViewModelImpl( messageId: messageId, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MessageDetailView(viewModel: viewModel) diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListViewModel.swift index ab4cce9b..29889127 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Messages/MessagesList/MessagesListViewModel.swift @@ -7,7 +7,7 @@ final class MessagesListViewModelImpl: MessageListViewModel { @Published var messages = [MessagesListViewState.Message]() @Published var error: FancyToast? - private let agent: EdgeAgent + private let agent: DIDCommAgent private var messagesDomain = Set() { didSet { messages = messagesDomain @@ -25,7 +25,7 @@ final class MessagesListViewModelImpl: MessageListViewModel { } private var cancellables = Set() - init(agent: EdgeAgent) { + init(agent: DIDCommAgent) { self.agent = agent bind() } @@ -123,7 +123,7 @@ final class MessagesListViewModelImpl: MessageListViewModel { let to = message.to else { return } - try await agent.registerDIDPair(pair: .init( + try await agent.edgeAgent.registerDIDPair(pair: .init( holder: from, other: to, name: nil diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift index b892e7bb..9cec520c 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Settings/SettingsViewRouter.swift @@ -8,7 +8,7 @@ struct SettingsViewRouterImpl: SettingsViewRouter { func routeToDIDs() -> some View { let viewModel = DIDListViewModelImpl( pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return DIDListView(viewModel: viewModel) @@ -18,7 +18,7 @@ struct SettingsViewRouterImpl: SettingsViewRouter { let viewModel = MediatorViewModelImpl( castor: container.resolve(type: Castor.self)!, pluto: container.resolve(type: Pluto.self)!, - agent: container.resolve(type: EdgeAgent.self)! + agent: container.resolve(type: DIDCommAgent.self)! ) return MediatorPageView(viewModel: viewModel) }